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);
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'm developing a Windows application (UWP) that has two pages, I want the best practice to pass parameters between pages.
it's my scenario:
We have two pages, each open and remain at the middle of the screen and a Button on each page, which send the message to the other page when we click on it.
I also want to pass information continuously and repeatedly.
in Page1.cs:
Page2 page2;
public Page1()
{
this.InitializeComponent();
CreatPage2();
}
// creat page 2
private async void CreatPage2()
{
var NewWindow = CoreApplication.CreateNewView();
int NewWindowid = 0;
await NewWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
{
Frame newframe = new Frame();
newframe.Navigate(typeof(Page2), this);
Window.Current.Content = newframe;
Window.Current.Activate();
ApplicationView.GetForCurrentView().Title = "page2";
NewWindowid = ApplicationView.GetForCurrentView().Id;
});
await Windows.UI.ViewManagement.ApplicationViewSwitcher.TryShowAsStandaloneAsync(NewWindowid);
}
//Button
private void ChangeP2_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page2
page2.TexBlock2.Text=$"From page1 :{e.ToString()}";
// change text color of the texblock in the page2
page2.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
in Page2.cs:
Page1 page1;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
page1 = e.Parameter as Page1;
base.OnNavigatedTo(e);
}
public Page2()
{
this.InitializeComponent();
}
//Button
private void ChangeP1_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page1
page1.TexBlock1.Text=$"From page2 :{e.ToString()}";
// change text color of the texblock in the page1
page1.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
the above code just work for the page2 to the page1. (it can change the textblock of pagea).
Please help me, I can't find a solution that work on two pages
Naah… the best way is to use a standard pattern that consist of an app ViewModel class, which contains all the common app data that you want to use in the logic layer.
I always do it like this:
1) I use the MainPage automatically created as the "shell" of the app, with a property that is the AppViewModel.
The MainPage (and thus the AppViewModel) can be accessed from everywhere in the app, by setting itself as a static field in its own class.
This is the code, simpler than you think:
public sealed partial class MainPage : Page
{
public AppViewModel ViewModel { get; set; } = new AppViewModel();
public static MainPage Current { get; set; }
public MainPage()
{
this.InitializeComponent();
Current = this;
}
}
2) The AppViewModel itself is a class that must implement the INotifyPropertyChanged interface, in order to enable bindable properties and functions.
It is common, among developers, to create a base class that implements it and then derive all the classes that needs bindable properties from it.
Here it is:
public class BaseBind : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] String propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}
Then you derive AppViewModel class (and all the other model and viewmodel classes) from it… populating it with all the common properties that you need to share across pages.
I have even added a derived property, in order to show how you can share even multiple data types at once, and a function:
public class AppViewModel : BaseBind
{
public AppViewModel()
{
// ...
}
// All common app data
private string sampleCommonString;
public String SampleCommonString
{
get { return sampleCommonString; }
set { SetProperty(ref sampleCommonString, value); OnPropertyChanged(nameof(SampleDerivedProperty1)); OnPropertyChanged(nameof(SampleDerivedProperty2)); }
}
public String SampleDerivedProperty1 => "return something based on SampleCommonString";
public String SampleDerivedProperty2
{
get
{
<<evaluate SampleCommonString>>
return "Same thing as SampleDerivedProperty1, but more explicit";
}
}
// This is a property that you can use for functions and internal logic… but it CAN'T be binded
public String SampleNOTBindableProperty { get; set; }
public void SampleFunction()
{
// Insert code here.
// The function has to be with NO parameters, in order to work with simple {x:Bind} markup.
// If your function has to access some specific data, you can create a new bindable (or non) property, just as the ones above, and memorize the data there.
}
}
3) Then, in order to access all this from another Page, just create an AppViewModel field in that page, as seen below:
public sealed partial class SecondPage : Page
{
public AppViewModel ViewModel => MainPage.Current.ViewModel;
public SecondPage()
{
this.InitializeComponent();
}
}
...and you can easily bind XAML controls properties to the AppViewModel itself:
<TextBlock Text="{x:Bind ViewModel.SampleCommonString, Mode=OneWay}"/>
<Button Content="Sample content" Click="{x:Bind ViewModel.SampleFunction}"/>
(Mode=OneWay is for real-time binding, in order that the property is immediately updated even in the UI, while Mode=TwoWay is used for those properties that can be edited from the control itself, by the user, in order to interact with app logic).
Hope this helped.
Best regards and happy new year.
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();
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 WPF Windows app. The viewmodel calls a method in the format of Model.TrySomething(), which returns a boolean if anything in TrySomething logically fails. If false is returned, the UI can throw a message back to the user.
What is the best way to bubble this message up from the model?
This is how we do it on our projects. Works fine:
// your event args might include more properties
public class ShowMessageBoxEventArgs : System.EventArgs
{
public string Title { get; set; }
public string Text { get; set; }
}
// example of your model base
public class MyModelBase
{
public event EventHandler<ShowMessageBoxEventArgs> ShowMessageBox;
protected void RaiseShowMessageBox(string title, string text)
{
if (ShowMessageBox == null)
return;
var _Args = new ShowMessageBoxEventArgs
{
Text = text,
Title = title
};
ShowMessageBox(this, _Args);
}
}
// for this sample, this is your view model
public class MyModel : MyModelBase
{
public void DoSomething()
{
// TODO: Do Something
base.RaiseShowMessageBox("DoSomething", "Complete!");
}
}
// this is your window or in app.xaml.cs (where we do it)
public partial class MainWindow : Window
{
MyModel m_MyModel = new MyModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = m_MyModel;
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
bool m_Loaded = false; // only once
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
if (m_Loaded)
return;
m_Loaded = true;
// allow model to show messagebox
m_MyModel.ShowMessageBox += (s, arg) =>
{
MessageBox.Show(arg.Text, arg.Title);
};
}
}
Best of luck!
If the message which you want to display is a modal dialog, you can write a service (lets name it MessageDialogService) which is injected in your viewmodel and then call a MessageDialogService.Show() method. This method creates a new WPF window and shows the message.
This service can then be used in any of your ViewModels to show messages.