In my app, I want to know in the View when Initialize is complete. The problem is that it gets launched before you can hook InitializeTask.PropertyChanged. Here is my ViewModel code:
public override async Task Initialize()
{
ClientID = await MyDataSource.GetClientID();
}
In my View I am doing the following:
protected override void OnViewModelSet()
{
var vm = this.DataContext as MyViewModel;
vm.InitializeTask.PropertyChanged += InitializeTask_PropertyChanged;
base.OnViewModelSet();
}
private void InitializeTask_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSuccessfullyCompleted")
{
vm.InitializeTask.PropertyChanged -= InitializeTask_PropertyChanged;
if (vm.ClientID != "")
posClient = new PosClient(this, vm.ClientID);
}
}
As far as I know, OnViewModelSet is the soonest that you have access to your ViewModel in your View. If I put in breakpoints, Initialize runs before OnViewModelSet is fired. This makes it very likely that Initialize has finished before you can hook the event. Is there another place in the View where you can hook the event before Initialize starts so you will be guaranteed to Initialize.PropertyChanged will fire in the View?
**** Update ****
I followed #fmaccaroni advice and implemented an MvxInteraction called DataLoaded. One thing I did different was to create a separate function to load the data.
public void LoadData()
{
Task.Run(async () =>
{
ClientID = await IHSDataSource.GetClientID();
_DataLoaded.Raise();
});
}
I was concerned about the async task finishing before I got the interaction event wired up. Doing it this way, I added this in the View.
protected override void OnViewModelSet()
{
vm = this.DataContext as InvoiceViewModel;
var set = this.CreateBindingSet<InvoiceView, InvoiceViewModel>();
set.Bind(this).For(view => view.DataLoaded).To(viewModel => viewModel.DataLoaded).OneWay();
set.Apply();
vm.LoadData();
base.OnViewModelSet();
}
This way, LoadData does not start until I am sure the result will trigger the interaction and I am guaranteed to get the result. This was the first I had heard about MvxInteraction and I am now using it all the time.
I'm not sure what you want to achieve, but if you want to take an action in your view when the Initialize ends just do an MvxInteraction and call it after your await, i.e.:
ViewModel:
private MvxInteraction _interaction = new MvxInteraction();
public IMvxInteraction MyMvxInteraction => _interaction;
public override async Task Initialize()
{
ClientID = await MyDataSource.GetClientID();
this._interaction.Raise();
}
View:
private IMvxInteraction _interaction;
public IMvxInteraction MyMvxInteraction
{
get => this._interaction;
set
{
if (this._interaction != null)
this._interaction.Requested -= this.OnInteractionRequested;
this._interaction = value;
this._interaction.Requested += this.OnInteractionRequested;
}
}
private void OnInteractionRequested(object sender, EventArgs e)
{
var vm = this.DataContext as MyViewModel;
if (vm.ClientID != "")
posClient = new PosClient(this, vm.ClientID);
}
and the binding in the view:
var set = this.CreateBindingSet<MyView, MyViewModel>();
set.Bind(this).For(view => view.MyMvxInteraction).To(viewModel => viewModel.MyMvxInteraction).OneWay();
set.Apply();
Related
I am trying to call this GetProductStatus() method on a page button click event, but it's loading before the button click. Means when the ViewModel is loading, this is also load automatically.
I would like to declared this VM method "GetProductStatus()" to be called only when a button click event occurs.
ViewModel method:
private async void GetProductStatus()
{
try
{
IsBusy = true;
var status = await ProductStatusService.GetProductStatus(new ProductStatus()
{
StoreCode = s_code,
StartTime = StartDateValue.AddMinutes(time1),
EndTime = StartDateValue.AddMinutes(time2)
});
IsBusy = false;
if (status != null)
{
//Process happens
}
else
{
//Array is Null
}
ProductStatus = status;
}
catch (Exception)
{
ProductStatus = null;
}
}
Here, the method is declared.
public ProductViewModel(INavigation nav, Store store)
{
_Nav = nav;
GetProductStatus();
}
Here, the clicked event.
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
I would like to declared this VM method "GetProductStatus()" to be
called only when a button click event occurs.
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
For above code you posted, we can find that the constructor of your viewmodel will be called as soon as you call code new ProductPage(_ViewModel).
So, you can try to remove code GetProductStatus(); in constructor ProductViewModel
public ProductViewModel(INavigation nav, Store store)
{
_Nav = nav;
// remove code here
//GetProductStatus();
}
and add a command in your ViewModel, and bind it to the button in your page.
Please refer to the following code:
public class ProductViewModel
{
public Command LoadDataCommand { get; set; }
public ProductViewModel() {
LoadDataCommand = new Command(loadData);
// remove code here
//GetProductStatus();
}
private void loadData()
{
GetProductStatus(); // add your code here
}
private async void GetProductStatus()
{
// other code
}
}
Note:
1.In this condition, you can also navigate as follows:
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
2.I don't add parameter to the constructor of ProductViewModel , you can modify above code I posted according to your needs.
Set aside the fact that you are working with views and models. Simply think of them like any other class in c#.
If you need to tell class A "do something under these circumstances`, what are your options?
Pass a parameter in constructor: public ProductViewModel(..., bool doGetProductStatus)..., usage: new ProductViewModel(..., true);
Call a method A.DoSomething(); after you've created it: _ViewModel.DoSomething();
Use MessagingCenter Publish/Subscribe.
I have an app with the following page structure:
MainPage
->SettingsPage
->InputLanguagePage
->OutputLanguagePage
All pages are ContentPages.
InputLanguagePage and OutputLanguagePage are the very same pages, so I don't want to program the very same page twice.
I can surely instantiate the same page twice, but I have no idea how I should get the return value.
I spent really long trying to describe my problem.
Perhaps the code below demonstrates the problem much better.
As you can see, I seem to be unable to get the return value of the LanguageSelectionPage since it's shown asynchronous.
Also changing it to Navigation.PushModalAsync() didn't change anything.
What would be the best way to have a ContentPage return a value or change a value and be notified about it?
Thank you.
MainPage:
public class MainPage : ContentPage
{
private async void OnSettingsSelected(object sender, EventArgs e)
{
SettingsPage nSettings = new SettingsPage();
await this.Navigation.PushAsync(nSettings);
}
(...)
SettingsPage:
public class SettingsPage : ContentPage
{
public SettingsPage()
{
protected override async void OnAppearing()
{
base.OnAppearing();
_btnInputLanguage.Clicked += async (sender, e) =>
{
LanguageSelectionPage nInputLanguage = new LanguageSelectionPage();
nInputLanguage.SelectedLCID = App.Settings.InputLanguageLCID;
await Navigation.PushAsync(nInputLanguage);
//The user can change "int SelectedLCID" within the ContentPage, but I have no idea how I could access it
return;
};
_btnOutputLanguage.Clicked += async (sender, e) =>
{
LanguageSelectionPage nOutputLanguage = new LanguageSelectionPage();
nOutputLanguage.SelectedLCID = App.Settings.OutputLanguageLCID;
await Navigation.PushAsync(nOutputLanguage);
//The user can change "int SelectedLCID" within the ContentPage, but I have no idea how I could access it
return;
};
(...)
LanguageSelectionPage:
public class LanguageSelectionPage : ContentPage
{
public int SelectedLCID { get; set; }
protected override async void OnAppearing()
{
base.OnAppearing();
(...)
I will give solutin to pass it ass parameter when yu ask to load page
public partial class InputLanguagePage : ContentPage
{
public InputLanguagePage(string Selection)
{
InitializeComponent();
}
}
public partial class OutputLanguagePage : ContentPage
{
public OutputLanguagePage(string Selection)
{
InitializeComponent();
}
}
In settingsPage Button Click
_btnInputLanguage.Clicked += async (sender, e) =>
{
Navigation.PushAsync(new InputLanguagePage ("SelectLanguage"));
return;
};
_btnOutputLanguage.Clicked += async (sender, e) =>
{
Navigation.PushAsync(new OutputLanguagePage("SelectLanguage"));
return;
};
There are a lot of ways to solve this:
pass a completion handler to the 2nd page
raise an event on the 2nd page and subscribe from the 1st page
pass an object whose value will be set by the 2nd page
use Messaging
*
// on the first page, listen for a message
MessagingCenter.Subscribe<SecondPage, string> (this, "ValueSet", (sender, arg) => {
// arg will contain the value passed by the sender
});
// when the value is set on the 2nd page, send a message
MessagingCenter.Send<SecondPage, string> (this, "ValueSet", someValue);
I initially had an async MVVM pattern; in debugging I've stripped it now down to only the following - synchronous - code:
XAML:
<Grid x:Name="LayoutRoot">
<Button x:Name="button" Content="{Binding Path=bText, FallbackValue=Initial}" Tapped="onTap"/>
</Grid>
C#
public partial class VPage : Page
{
public ViewModel viewModel;
public VPage()
{
DataContext = viewModel = new ViewModel();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e) { viewModel.onNavigatedTo(); }
private void onTap(object sender, TappedRoutedEventArgs eventArgs) { viewModel.onTap(); }
}
public class viewModel : INotifyPropertyChanged
{
private String _bText;
public String bText { get { return _bText; } set { _bText = value; DB.major("ViewModel: bText=" + _bText); NotifyPropertyChanged("bText"); } }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged == null)
writeln("Null handler!");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
public void onNavigatedTo() { bText = "Updated via onNavigatedTo"; }
private int count = 0;
public void onTap() { bText = "Updated via onTap " + count++.ToString(); }
}
That's the entire code base now - I've eliminated all of the Model and Async code to aid in debugging.
When initiated the Page constructor runs and completes.
Subsequently, VPage.OnNavigatedTo gets called, and invokes ViewModel.onNavigatedTo();
On the call to OnNavigatedTo, the writeln triggers, indicating that the handler is null, suggesting that the component initialization hasn't finished in some way. Subsequently - ie onTap - all works fine and the handler is in place. Interestingly, the field is updated to "Updated via onNavigatedTo" even though the null handler writeln triggered.
When I had the full Async pattern in place I thought that it was a threading issue but now it's obvious that it's something much simpler.
I don't think you've given the UI enough time to register its handlers. A quick fix is to yield. Try this:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
// This forces the rest of your code to enqueue after UI is done.
await Task.Yield();
viewModel.populate();
}
UPDATE
The bindings will not activate until your page is in the visual tree. They may or may not retrieve the value first, but they'll subscribe to PropertyChanged later.
If you want to ensure that all bindings are in place, then don't subscribe to OnNavigatedTo, but use Loaded instead.
public partial class VPage : Page
{
public ViewModel viewModel;
public VPage()
{
DataContext = viewModel = new ViewModel();
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
viewModel.onNavigatedTo();
}
private void onTap(object sender, TappedRoutedEventArgs eventArgs) { viewModel.onTap(); }
}
I have a page and on clicking a plus button on toolbar i am calling a popup page
from popup page user can add a new entry or cancel / close window without doing anything
Everything is working fine and code is like this
public partial class SelectSchool : ContentPage
{
public SelectSchool()
{
InitializeComponent();
#region toolbar
ToolbarItem tbi = null;
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
Navigation.PushModalAsync(target_page);
}, 0,0);
}
ToolbarItems.Add(tbi);
#endregion
this.Title = "Select School";
}
}
And my popup page is like
public partial class AddSchool : ContentPage
{
public AddSchool()
{
InitializeComponent();
}
private async void Button_OK_Clicked(object sender, EventArgs e)
{
//doing some operations like entry to db etc and close page
Navigation.PopModalAsync();
}
private void cancelClicked(object sender, EventArgs e)
{
Navigation.PopModalAsync();
}
}
But now i want to wait for the Popup to get closed to do some additional coding and i tried below code
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
await Navigation.PushModalAsync(target_page);
//await till target_page is closed and once its closed call my next function here
}, 0,0);
}
But await is not working . How can i await on this area till the popup getting closed ? Any idea??
Use the Disappearing event on your modal page.
Example:
var modalPage = new ContentPage();
modalPage.Disappearing += (sender2, e2) =>
{
System.Diagnostics.Debug.WriteLine("The modal page is dismissed, do something now");
};
await content.Navigation.PushModalAsync(modalPage);
System.Diagnostics.Debug.WriteLine("The modal page is now on screen, hit back button");
Or use a EventWaitHandle:
var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
var modalPage = new ContentPage();
modalPage.Disappearing += (sender2, e2) =>
{
waitHandle.Set();
};
await content.Navigation.PushModalAsync(modalPage);
System.Diagnostics.Debug.WriteLine("The modal page is now on screen, hit back button");
await Task.Run(() => waitHandle.WaitOne());
System.Diagnostics.Debug.WriteLine("The modal page is dismissed, do something now");
A bit late on the answer here, but it might be best to listen to the Application's OnModalPagePopping event handler.
First, create the modal page. Give it a property to store the data you want to later retrieve:
public class MyModalPage : ContentPage
{
public string Data { get; set; }
public MyModalPage()
{
InitializeComponent();
// ... set up the page ...
}
private async void PopThisPage()
{
// When you want to pop the page, just call this method
// Perhaps you have a text view with x:Name="PhoneNumber", for example
Data = PhoneNumber.Text; // store the "return value" before popping
await MyProject.App.Current.MainPage.Navigation.PopModalAsync();
}
}
In the parent page, you can create the modal page, and set up the event handler to listen for when the modal page pops:
public class MyPage : ContentPage
{
MyModalPage _myModalPage;
public MyPage()
{
InitializeComponent();
// ... set up the page ...
}
private async void ShowModalPage()
{
// When you want to show the modal page, just call this method
// add the event handler for to listen for the modal popping event:
MyProject.App.Current.ModalPopping += HandleModalPopping;
_myModalPage = new MyModalPage();
await MyProject.App.Current.MainPage.Navigation.PushModalAsync(_myModalPage());
}
private void HandleModalPopping(object sender, ModalPoppingEventArgs e)
{
if (e.Modal == _myModalPage)
{
// now we can retrieve that phone number:
var phoneNumber = _myModalPage.Data;
_myModalPage = null;
// remember to remove the event handler:
MyProject.App.Current.ModalPopping -= HandleModalPopping;
}
}
}
This is better than using the OnDisappearing method, as others have already stated that can be called when the app is backgrounded, etc. And its behavior is not consistent across platforms.
There is also another event OnModalPopped, which is called after the modal is completely popped from the navigation stack. If using that, it should work similarly.
You can try to create an event, call when pop close.
public partial class AddSchool : ContentPage
{
public delegate void PopupClosedDelegate();
public event PopupClosedDelegate PopupClosed;
public AddSchool()
{
InitializeComponent();
}
private async void Button_OK_Clicked(object sender, EventArgs e)
{
//doing some operations like entry to db etc and close page
await Navigation.PopModalAsync();
if (PopupClosed!=null)
{
PopupClosed();
}
}
private async void cancelClicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
if (PopupClosed != null)
{
PopupClosed();
}
}
}
I put it on the button click event, maybe you can put on close or dispose event. Then here is implement
public partial class SelectSchool : ContentPage
{
public SelectSchool()
{
InitializeComponent();
#region toolbar
ToolbarItem tbi = null;
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
target_page.PopupClosed += () => { /*Do something here*/ };
Navigation.PushModalAsync(target_page);
}, 0, 0);
}
ToolbarItems.Add(tbi);
#endregion
this.Title = "Select School";
}
}
Hope this help.
I create an extension method for this:
public static class DialogUtils
{
public static async Task ShowPageAsDialog(this INavigation navigation, Page page)
{
int pagesOnStack = navigation.NavigationStack.Count + 1;
var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
page.Disappearing += (s, e) =>
{
if (navigation.NavigationStack.Count <= pagesOnStack)
waitHandle.Set();
};
await navigation.PushAsync(page);
await Task.Run(() => waitHandle.WaitOne());
}
}
And I can use it:
private async void bShowDialogPage_Clicked(object sender, EventArgs e)
{
var page = new DialogPage();
await page.LoadData();
await Navigation.ShowPageAsDialog(page);
var result = page.PageResult;
}
It supports situations when dialog page show another page. I prefer NavigationStack instead ModalStack due to NavigationPage and BackButton.
Another way of accomplishing this is by calling an event from the page's OnDisapearing method, this event can then be subscribed by the navigation service which you create, and you can then use the "TaskCompletionSource" to wati until your page finishes its work and then complete the task.
For more details about accomplishing this, you can check this blog post.
Here is the base page's implementation, every page in this demo app inherit this page:
public class BasePage<T> : ContentPage
{
public event Action<T> PageDisapearing;
protected T _navigationResut;
public BasePage()
{
}
protected override void OnDisappearing()
{
PageDisapearing?.Invoke(_navigationResut);
if (PageDisapearing != null)
{
foreach (var #delegate in PageDisapearing.GetInvocationList())
{
PageDisapearing -= #delegate as Action<T>;
}
}
base.OnDisappearing();
}
}
Here is an overview of the navigation service you should use:
public async Task<T> NavigateToModal<T>(string modalName)
{
var source = new TaskCompletionSource<T>();
if (modalName == nameof(NewItemPage))
{
var page = new NewItemPage();
page.PageDisapearing += (result) =>
{
var res = (T)Convert.ChangeType(result, typeof(T));
source.SetResult(res);
};
await App.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page));
}
return await source.Task;
}
To call this page with the navigation service, you can use the following code:
var item = await new SimpleNavigationService().NavigateToModal<Item>(nameof(NewItemPage));
Items.Add(item);
I have a ViewModel like this:
public class WelcomeWindowVm : ViewModel
{
private ViewModel view;
public WelcomeWindowVm(){
this.View = new LoginVm() {
Completed += (o, e) => {
this.View = new OtherVm(e.User){
Completed += (o, e) =>; // and so on
}
}
};
}
public ViewModel View {
get {
return this.view;
}
set {
this.view = value;
this.OnPropertyChanged(nameof(this.View));
}
}
}
LoginVm is another Viewmodel whose Completed event is triggered when a Command on it is completed (The event is only triggered when correct login credentials are used). OtherVm is another vm whose completed event is triggered for whatever reason.
I render the View using a DataTemplate. For example:
<Window.Resources>
<DataTemplate DataType="vm:LoginVm">
Textboes and buttons here
</DataTemplate>
<DataTemplate DataType="vm:OtherVm">
...
</DataTemplate>
</Window.Resources>
<ContentControl Content={Binding View} />
The DataContext of this window is set to WelcomeWindowVm class above, before ShowDialog.
This works well. When the Window is shown using ShowDialog, LoginVm is shown. Then OtherVm when whatever task of LoginVm is completed, and so on.
Now I thought of converting the Completion stuff to Async/await pattern. The LoginVm now looks like this:
public LoginVm{
...
private TaskCompletionSource<User> taskCompletionSource = new TaskCompletionSource<User>();
...
// This is the Relay command handler
public async void Login()
{
// Code to check if credentials are correct
this.taskCompletionSource.SetResult(this.user);
// ...
}
public Task<User> Completion(){
return this.taskCompletionSource.Task;
}
}
Instead of this:
public LoginVm{
public event EventHandler<CustomArgs> Completed;
// This is the Relay command handler
public async void Login()
{
// Code to check if credentials are correct
OnCompleted(this.user);
// ...
}
}
So that I can use it like this:
public WelcomeWindowVm(){
var loginVm = new LoginVm();
this.View = new LoginVm();
User user = await loginVm.Completion();
var otherVm = new OtherVm(user);
this.View = otherVm;
Whatever wev = await otherVm.Completion();
//And so on
}
But I can't use await in a Constructor and even if I use an async Method for that, how will I call it in another class after calling ShowDialog since ShowDialog blocks?
I think using an async void will work. But from what I have heard, it should be avoided unless I am using it in an event handler.
Maybe use an async Task method but not await it?
You can do it like this:
public WelcomeWindowVm() {
var loginVm = new LoginVm();
this.View = loginVm;
loginVm.Completion().ContinueWith(loginCompleted =>
{
var otherVm = new OtherVm(loginCompleted.Result);
this.View = otherVm;
otherVm.Completion().ContinueWith(whateverCompleted =>
{
});
});
}