Binding image source in Xamarin when using async data - image

I am struggling to set the image source to the value I receive from the database.
Here are relevant parts of my XAML, its code-behind and its view model.
XAML:
<Label Text="{Binding ViewModel_Fid}" />
<Image Source="{Binding ViewModel_ImageStream}" />
Code-behind:
protected override void OnAppearing() {
base.OnAppearing();
myViewModel = new myViewModel();
myViewModel.PopulateFid();
BindingContext = myViewModel;
}
View Model:
public event PropertyChangedEventHandler PropertyChanged;
private string _fid;
public async void PopulateFid() {
_fid = await getFid();
PropertyChanged(this, new PropertyChangedEventArgs(nameof(ViewModel_Fid)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(ViewModel_ImageStream)));
public MemoryStream ViewModel_ImageStream {
get {
byte[] buffer = myGetBytes(_fid);
return null == buffer ? null : new MemoryStream(buffer);
}
}
The problem is, ViewModel_ImageStream systematically executes BEFORE PopulateFid, which means that in ViewModel_ImageStream I always get _fid = null.
I am pretty sure this is due to PopulateFid being async, but I need it to be this way, because getFid() is an external async function.
How can I enforce that PopulateFid executes before ViewModel_ImageStream is set?
Thanks!
PS. See my solution/answer below.

What ultimately worked for me was adding the Image element at runtime (i.e., not specifying it in XAML).
I add it once I've retrieved the necessary data in View Model.
So in Code-behind:
protected override void OnAppearing() {
base.OnAppearing();
myViewModel = new myViewModel();
myViewModel.PopulateFid();
BindingContext = myViewModel;
myViewModel.LoadImage = (obj) => {
var img = new Image();
img.Source = new StreamImageSource() {
Stream = (token) => getstream(token)
};
mainstack.Children.Add(img);
};
}
private async Task<Stream> getstream(object token) {
return new MemoryStream(myViewModel.myGetBytes);
}
in View Model:
private string _fid;
public async void PopulateFid() {
_fid = await getFid();
LoadImg?.Invoke(true);
}

Related

How can I use a Xamarin DataTemplate with an ObservableCollection instead of a List?

I have a page that looks like this. I tested out the code using a List but I need to use an ObservableCollection as the contents of the data will change after it's initially populated. My problem is that when I change from a List to ObservableCollection I no longer see any data appearing.
public YourPage()
{
var viewModel = _vm = new YourPageViewModel()
var dataTemplate = new DataTemplate(()=>
{
var mygrid = new MyGrid ();
mygrid.SetBinding(MyGrid.TextProperty, "Text");
return mygrid;
});
StackLayout stackLayout = new StackLayout();
BindableLayout.SetItemsSource(stackLayout, _vm.ListOfText);
BindableLayout.SetItemTemplate(stackLayout, dataTemplate);
Content = stackLayout;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.OnAppearing();
}
And a ViewModel
public partial class YourPageViewModel : BaseViewModel
{
private ObservableCollection<TestModel> _listOfText;
public ObservableCollection<TestModel> ListOfText{
get => _listOfText;
set => SetProperty(ref _listOfText, value);
}
public void OnAppearing()
{
var tempList = ...;
ListOfText = new ObservableCollection<TestModel>(tempList);
}
}
Given this situation with an ObservableCollection then the data template doesn't seem to show any data.
Does anyone have an idea what might be wrong?
You will want to create the ObservableCollection only once. If you use data-binding on it, it will subscribe to certain events that will emit the changes in the collection.
By doing ListOfText = new ObservableCollection<TestModel>(tempList); it will cause those events to be disconnected and your data won't show up. Instead, change it to be more like this
public partial class YourPageViewModel : BaseViewModel
{
// This changed
private ObservableCollection<TestModel> _listOfText = new ObservableCollection<TestModel>(tempList);
public ObservableCollection<TestModel> ListOfText{
get => _listOfText;
set => SetProperty(ref _listOfText, value);
}
public void OnAppearing()
{
var tempList = ...;
// This changed
ListOfText.Clear();
foreach (var text in tempList)
ListOfText.Add(text);
}
}
Notice how I only create the ObservableCollection once and repopulate each time instead of creating a new one.
Becasue when you set the data-binding in the constructor of your page,the ListOfText is not populated the data until you call _vm.OnAppearing(); in OnAppearing() method.
You could try to pipulate the data in the constructor of your viewmodel:
public partial class YourPageViewModel : BaseViewModel
{
private ObservableCollection<TestModel> _listOfText;
public ObservableCollection<TestModel> ListOfText{
get => _listOfText;
set => SetProperty(ref _listOfText, value);
}
public YourPageViewModel ()
{
var tempList = ...;
_listOfText = new ObservableCollection<TestModel>(tempList);
}
}
and then you don't need to call _vm.OnAppearing(); in OnAppearing() method.
You need to make sure that you have populated your data when you bind it.
The solutions given work for when the data is populate one time after the binding is set up but then after that the layout will not be refreshed.
To solve this problem what is needed is to place the DataTemplate in a CollectionView
CollectionView collectionView = new CollectionView();
collectionView.SetBinding(ItemsView.ItemsSourceProperty, "ListOfText");
collectionView.ItemTemplate = new DataTemplate(() =>
{
var mygrid = new MyGrid ();
mygrid.SetBinding(MyGrid.TextProperty, "Text");
return mygrid;
});
Content = collectionView;
When this is done the ObservableCollection can be refreshed at any time and the screen layout will change when it's refreshed.

Xamarin Forms MessagingCenter Subscribe called two times

I'm clicking on a product item in listview in product page viewmodel to show a popup(using rg.plugin popup) for selecting one of the product variants.After selecting variant,i am sending the selected variant to product page using messagingcenter from variant popup page viewmodel,subscribed in product page viewmodel constructor. working fine there.when i navigate to the previous page and then came back to this product page for adding one or more variant to the
same previously selected product,Messagingcenter subscribe called twice and product value increased twice.Tried to subscribe in the product page onappearing and unsubscribe in disappearing method.still calling two times? How to solve this issue?
calling popup:
var result = await dataService.Get_product_variant(store_id, product_id);
if (result.status == "success")
{
ind_vis = false;
OnPropertyChanged("ind_vis");
App.Current.Properties["product_variant_result"] = result;
App.Current.Properties["cartitems"] = purchaselist;
App.Current.Properties["selected_product"] = product_List2 ;
await PopupNavigation.Instance.PushAsync(new Popup_variant());
}
popup viewmodel: sending message
public Popup_variant_vm()
{
Radio_btn = new Command<Product_variant_list2>(Radio_stk_tapped);
product_variant_list = new List<Product_variant_list2>();
purchaselist = new ObservableCollection<Product_list2>();
show_variants();
}
internal void Confirm_variant()
{
if(App.Current.Properties.ContainsKey("selected_variant"))
{
MessagingCenter.Send<Popup_variant_vm, object>(this, "selected_variant", App.Current.Properties["selected_variant"]); //Message send from popup to product page
}
else
{
DependencyService.Get<IToast>().LongAlert("Please select any size");
}
}
product page viewmodel: subscribed here..called twice when navigating from previous page to this
public Store_page()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
var vm = new store_page_vm();
vm.Navigation = Navigation;
BindingContext = vm;
MessagingCenter.Unsubscribe<Popup_variant_vm, object>(this, "selected_variant");
MessagingCenter.Subscribe<Popup_variant_vm, object>(this, "selected_variant",async (sender, selected_variant) =>
{
var vm1 = BindingContext as store_page_vm;
vm1?.Addcart2(selected_variant);// called twice
});
}
unsubscribed in product cs page
protected override void OnDisappearing()
{
var vm = BindingContext as store_page_vm;
vm?.Save_cart();
MessagingCenter.Unsubscribe<Popup_variant_vm>(this, "selected_variant");
}
Your unsubscription should look something like below and it should work :
MessagingCenter.Unsubscribe<Popup_variant_vm, object>(this, "selected_variant");
https://stackoverflow.com/a/44753021/10937160
try this, and make sure you do not call Subscribe more than once.
My solution:
put unsubscribe sentence into subscribe body !!
MessagingCenter.Subscribe<object, string>(this, "IdSearch", (sender, arg) =>
{
listView.ItemsSource = arg;
MessagingCenter.Unsubscribe<object, string>(this, "IdSearch");
}, BindingContext);
I have created static counter variable in my app the in subscriber I have done this:
public static class Constants
{
public static int msgCenterSubscribeCounter { get; set; } = 0;
}
MessagingCenter.Subscribe<object, string>(this, "hello", (sender, arg) =>
{
Constants.msgCenterSubscribeCounter++;
if (arg.Equals("hello") && Constants.msgCenterSubscribeCounter == 1)
{
// handle your logic here
}
});
Reset counter in OnDisappearing() method from where you have called Send.
Changing Messagingcenter in to single subscription.
public class Messagingcenter_singleton
{
private static Messagingcenter_singleton _instance;
private bool isActivated = false;
private Action<string> callBackFun = null;
public static Messagingcenter_singleton Instance()
{
if (_instance == null)
{
_instance = new Messagingcenter_singleton();
}
return _instance;
}
public void setCallBack(Action<string> eventCallBack)
{
callBackFun = eventCallBack;
}
public void startSubscribe()
{
if (!isActivated)
{
isActivated = true;
MessagingCenter.Subscribe<string, string>(this, "Name", eventCallBack);
}
}
private void eventCallBack(string arg1, string arg2)
{
if (callBackFun != null)
{
InvokeMethod(new Action<string>(callBackFun), arg2);
}
}
public static object InvokeMethod(Delegate method, params object[] args)
{
return method.DynamicInvoke(args);
}
}
Use Below Code in you view model class
public void initSubscribe()
{
Messagingcenter_singleton.Instance().startSubscribe();
Messagingcenter_singleton.Instance().setCallBack(eventCallBack)
}
public void eventCallBack(string arg2)
{
// write your code here
}

Initialize an xamarin view after an async method

Good evening everyone.
For some time now I have been to Xamarin. My first tests are rather conclusive. I decided to try to make a small application that retrieves information in a database via an API and then update this data via a ListView.
When I launch the application on my emulator everything works but as soon as I install the application on my phone it crashes. I thought this was because the API but I have an API that I use to check the Login / password that works correctly.
The API that returns the data reviews a lot of line about 3500/4000, can this be the reason?
So I passed the loading of the data in my viewModel in an async method but the problem now is that the view loads before the data is loaded correctly. Is there a way to get the view initialized after the data is loaded?
Below my code.
Initializing my viewModel
class ManageInventViewModel
{
public ObservableCollection<InventViewModel> InventProduct { get; set; }
public Command<InventViewModel> UpdateCommand
{
get
{
return new Command<InventViewModel>(invent =>
{
var index = invent.IndexLigneInventaire;
InventProduct.Remove(invent);
InventProduct.Insert(index, invent);
});
}
}
public Command<InventViewModel> ResetStock
{
get
{
return new Command<InventViewModel>(invent =>
{
var index = InventProduct.IndexOf(invent);
InventProduct.Remove(invent);
invent.RealStockProduct = 0;
InventProduct.Insert(index, invent);
});
}
}
public ManageInventViewModel()
{
LoadInventaire();
}
private async void LoadInventaire()
{
var listMvt = await Utils.Utils.GetListMouvementUntilDate();
var listStock = Utils.Utils.GetStockByProduct(listMvt).Take(20);
InventProduct = new ObservableCollection<InventViewModel>();
var indexLine = 0;
foreach (var stock in listStock)
{
var inventViewModel = new InventViewModel
{
LibelleProduit = stock.LibelleProduit,
PrCodeProduit = stock.PrCodeProduit,
UpCodeProduit = stock.UpCodeProduit,
RealStockProduct = stock.StockTheoProdct,
StockTheoProdct = stock.StockTheoProdct,
IndexLigneInventaire = indexLine
};
++indexLine;
InventProduct.Add(inventViewModel);
}
}
}
Initializinz my view
public partial class InventPage : ContentPage
{
public InventPage()
{
InitializeComponent();
TableInvent.ItemSelected += (sender, e) =>
{
if (TableInvent.SelectedItem != null)
{
if (TableInvent.SelectedItem is InventViewModel item)
{
PopupNavigation.Instance.PushAsync(new ChangeStockModal(item, this));
}
TableInvent.SelectedItem = null;
}
};
}
private void Reset_Stock(object sender, EventArgs e)
{
var input = sender as Button;
var inventViewModel = input?.BindingContext as InventViewModel;
var listViewModel = BindingContext as ManageInventViewModel;
listViewModel?.ResetStock.Execute(inventViewModel);
}
public void Update_Stock_List(InventViewModel dataStockUpdate)
{
var listViewModel = BindingContext as ManageInventViewModel;
listViewModel?.UpdateCommand.Execute(dataStockUpdate);
PopupNavigation.Instance.PopAsync();
}
}
Thanks
I managed to create the ActivityIndicator but I can not get my data loaded while I'm displaying the wait screen.
Regarding this issue, I don't see you useActivityIndicator from your code,maybe you didn't update your code, I think if you use useActivityIndicator , You can bind one property to ActivityIndicator IsRunning and IsVisible, then you can solve your issue.
Related use ActivityIndicator step, you can take a look:
ActivityIndicator

Xamarin Prism cannot navigate with parameters

for my app i create my own buttons using a frame and adding a tapgesture to it. here i use the navigation of prism to go to a specific page with a parameter. however. the viewmodel i'm going to does not trigger the Navigated to method. here is some code.
during debugging it seems that the adding of the parameters is no problem. however the constructor for the viewmodel is called instead.
button
public class FolderButton : Frame
{
public FolderButton(Folder folder, INavigationService navigationService)
{
var navParams = new NavigationParameters();
navParams.Add("folder", folder);
GestureRecognizers.Add(new TapGestureRecognizer()
{
Command = new Command(async () => { await navigationService.NavigateAsync("FolderInventory", navParams); }),
});
BackgroundColor = Color.CornflowerBlue;
var thickness = new Thickness();
thickness.Bottom = 10;
thickness.Left = 10;
thickness.Right = 10;
thickness.Top = 10;
Margin = thickness;
CornerRadius = 5;
var completeStack = new StackLayout();
var imgStack = new StackLayout();
imgStack.Padding = thickness;
imgStack.Children.Add(new Image { Source = "folder.png" });
completeStack.Children.Add(imgStack);
var lblStack = new StackLayout();
lblStack.Padding = thickness;
lblStack.Children.Add(new Label
{
Text = folder.Name,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Start
});
completeStack.Children.Add(lblStack);
Content = completeStack;
}
}
called viewmodel
public class FolderInventoryViewModel : BindableBase, INavigatedAware
{
public Folder Folder => _folder;
private readonly INavigationService _navigationService;
private Folder _folder;
private readonly ISQLiteService _sqlService;
private List<Frame> _buttons;
public List<Frame> Buttons
{
get => _buttons;
set => _buttons = value;
}
public FolderInventoryViewModel(Folder folder, INavigationService navigationService, ISQLiteService sqlService)
{
_folder = folder;
_sqlService = sqlService;
_navigationService = navigationService;
GetItemsForFolder();
}
private void GetItemsForFolder()
{
var itemList = _sqlService.GetAllFolderItems(Folder.Name);
foreach (var item in itemList)
{
var itemButton = new ItemButton(_navigationService, item);
_buttons.Add(itemButton);
}
}
public void OnNavigatedFrom(NavigationParameters parameters)
{
if (parameters["folder"] is Folder folder)
{
_folder = folder;
}
}
public void OnNavigatedTo(NavigationParameters parameters)
{
if (parameters["folder"] is Folder folder)
{
_folder = folder;
}
}
}
This is not the essence of using the framework. To properly use the Prism with its NavigationParameters you first properly maintain the MVVM idea behind it.
E.g.
<Button Command="{Binding testCommand}" text="TestButton"/>
Your ViewModel (Pardon about this, you need to inject NavigationService to your ViewModel's constructor)
private DelegateCommand _testCommand;
public DelegateCommand testCommand =>
_testCommand?? (_testCommand= new DelegateCommand(ExecuteTest));
private void ExecuteTest()
{
NavigationParameters navigationParameters = new NavigationParameters();
navigationParameters.Add("yourParameterId", value);
NavigationService.NavigateAsync("YourPage", navigationParameters);
}
And then onto your next page
Inherit INavigationAware to your NextPage : YourNextPage: BaseViewModel, INavigationAware
INavigationAware has 3 methods NavigatingTo, NavigatedTo, NavigatedFrom
Inside OnNavigatedTo you can call the parameters you have passed
public void OnNavigatedTo(NavigationParameters parameters)
{
//You can check if parameters has value before passing it inside
if(parameters.ContainsKey("YourParameterId")
{
yourItem = (dataType)parameters[YourParameterId];
}
}
Also note: The constructor will always be called first before the Navigating methods

UIDocumentPickerViewController selection in Xamarin Forms

I'm stuck on getting my DocumentPicker fully working. Right now it presents the view controller but I can't figure out how to wait or get the result.
In swift you just write the void documentPicker(UIDocumentPickerViewController controller, didPickDocumentAtUrl... method and when it's finished it goes to there.
But in Xamarin it must not be that simple. I've written that method, from the class I'm calling it from as well as in my AppDelegate.cs class and as well as in my Main.cs class. None seem to work, unless I've written it wrong.
What I have is this ....
public async Task<string> pickResume()
{
string path = string.Empty;
var controller = new UIViewController();
var docVC = new UIDocumentPickerViewController(new string[] { "org.openxmlformats.wordprocessingml.document", "com.microsoft.word.doc" }, UIDocumentPickerMode.Import);
UIViewController topController = getTopViewController();
topController.PresentViewController(docVC, true, null);
return path;
}
void documentPicker(UIDocumentPickerViewController controller, NSUrl didPickDocumentAtURL)
{
Console.WriteLine("done");
}
getTopViewController() is just a helper method to get the top view controller so I can present the DocumentPicker
Figured it out, and it's a lot easier than I was making it out to be.
The UIDocumentPickerViewController has two EventHandlers, DidPickDocument and WasCancelled so I just assigned those to two different methods and done.
public async Task<string> pickResume()
{
string path = string.Empty;
var controller = new UIViewController();
var docVC = new UIDocumentPickerViewController(new string[] { "org.openxmlformats.wordprocessingml.document", "com.microsoft.word.doc" }, UIDocumentPickerMode.Import);
docVC.DidPickDocument += DocVC_DidPickDocument;
docVC.WasCancelled += DocVC_WasCancelled;
UIViewController topController = getTopViewController();
topController.PresentViewController(docVC, true, null);
return await GetDocPath(new CancellationTokenSource());
}
private void DocVC_WasCancelled(object sender, EventArgs e)
{
//Handle being cancelled
}
private void DocVC_DidPickDocument(object sender, UIDocumentPickedEventArgs e)
{
//Handle document selection
}

Resources