I am developing Windows Phone 8.1 app with MVVM.
I have base view model class which contains Navigation Service as below:
public abstract class BaseViewModel : INotifyPropertyChanged
{
protected readonly INavigationService NavigationService;
//....
}
There is my navigation service class:
public class NavigationService : INavigationService
{
public void Navigate(Type destinationPage)
{
((Frame)Window.Current.Content).Navigate(destinationPage);
}
public void Navigate(Type desitnationPage, object parameter)
{
((Frame)Window.Current.Content).Navigate(desitnationPage, parameter);
}
public void GoBack()
{
((Frame)Window.Current.Content).GoBack();
}
}
Everything is working fine when I am binding commands from XAML. There is problem when I want to override BackButton. I have also created base page model which also contains NavigationService. Each page has an overridde pf BackPressed as below:
public class BasePage : Page
{
protected INavigationService NavigationService => ComponentManager.GetInstance<INavigationService>();
public BasePage()
{
//...
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
protected virtual void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
//Frame.Navigate(typeof(MainPage));
(this.DataContext as BaseViewModel)?.Back.Execute(sender);
}
}
As you see in HardwareButtons_BackPressed method I've tried to make it in to ways but none is workings. Every time I press back button application crashes without any error.
I don't think the app is crashing, it's just exiting because that is the default behaviour of the back button.
What you need to do is flag that you've handled the back button by adding this line of code in your BackPressed event handler:
e.Handled = true;
Related
I'm trying to implement a lifecycle effect in Xamarin.Forms, but am having trouble for the iOS version. For some reason, I can't seem to observe the window changing notification event. Below is my code:
public class CustomLifeCycleEffectRouter : PlatformEffect
{
private const NSKeyValueObservingOptions ObservingOptions = NSKeyValueObservingOptions.Initial | NSKeyValueObservingOptions.New;
UIView? _nativeView;
CustomLifeCycleEffect? _lifeCycleEffect;
IDisposable _isLoadedObserverDisposable;
protected override void OnAttached()
{
_lifeCycleEffect = Element.Effects.OfType<CustomLifeCycleEffect>().FirstOrDefault() ?? throw new ArgumentNullException($"The effect {nameof(CustomLifeCycleEffect)} can't be null.");
_nativeView = Control ?? Container;
_isLoadedObserverDisposable = _nativeView?.AddObserver("window", ObservingOptions, isWindowAttachedObserver);
}
protected override void OnDetached()
{
_lifeCycleEffect?.RaiseUnloadedEvent(Element);
_isLoadedObserverDisposable.Dispose();
}
private void isWindowAttachedObserver(NSObservedChange nsObservedChange)
{
if (_nativeView.Window != null)
_lifeCycleEffect?.RaiseLoadedEvent(Element);
else
_lifeCycleEffect?.RaiseUnloadedEvent(Element);
}
}
I am well aware that the Xamarin.Community Toolkit has a similar effect, but it fires the event to early; I need it to fire when I can navigate up the hiearchy to the root parent. Can anybody see a problem?
Edit
I've created a small sample to replicate my behaviours and issues. It can be viewed here:
https://github.com/sonic1015/LifeCycleEffectTesting
The goal is to only have the following messages in the debug output:
$"{elementName} is already a page."
$"{elementName} is a child of {pageName}."
and NOT these ones:
$"{elementName} does not have a parent ???."
$"How can {elementName} be loaded and not have a parent in hierarchy ???."
$"WTF??? we never loaded {elementName}."
These messages can be found in the "ViewExtensions" class, and I've the goal is to have every user-created view fire off good messages.
One thing I've noticed:
I also included a variant of the Xamarin Community Toolkit version of the router effect in the platform project, and it actually works, with the exception that it seems if any views are templated, it will fire "loaded" when it does not yet have a parent. I think this is why it originally didn't work for me, so if I could figure out a way to work that little edge case out, I could use that version of the routing effect.
1.Create a ViewLifecycleEffect class that implements RoutingEffect in the shared project like below:
public class ViewLifecycleEffect : RoutingEffect
{
public const string EffectGroupName = "XFLifecycle";
public const string EffectName = "LifecycleEffect";
public event EventHandler<EventArgs> Loaded;
public event EventHandler<EventArgs> Unloaded;
public ViewLifecycleEffect() : base($"{EffectGroupName}.{EffectName}") { }
public void RaiseLoaded(Element element) => Loaded?.Invoke(element, EventArgs.Empty);
public void RaiseUnloaded(Element element) => Unloaded?.Invoke(element, EventArgs.Empty);
}
2.In Mainpage.xmal:
<StackLayout x:Name="MainContainer" Margin="20" VerticalOptions="Center" HorizontalOptions="Center">
<Button Text="CLICK TO REMOVE" Clicked="Button_OnClicked" HorizontalOptions="Center" VerticalOptions="Center">
<Button.Effects>
<effects:ViewLifecycleEffect Loaded="ViewLifecycleEffect_OnLoaded" Unloaded="ViewLifecycleEffect_OnUnloaded"/>
</Button.Effects>
</Button>
</StackLayout>
Code-behind:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void Button_OnClicked(object sender, EventArgs e)
{
MainContainer.Children.Clear();
}
private void ViewLifecycleEffect_OnLoaded(object sender, EventArgs e)
{
DisplayAlert("LOADED", "Button was added", "OK");
}
private void ViewLifecycleEffect_OnUnloaded(object sender, EventArgs e)
{
DisplayAlert("UNLOADED", "Button was removed", "OK");
}
}
3.Create a IosLifecycleEffect in the iOS project.
[assembly:ResolutionGroupName(ViewLifecycleEffect.EffectGroupName)]
[assembly:ExportEffect(typeof(IosLifecycleEffect), ViewLifecycleEffect.EffectName)]
namespace XFLifecycle.iOS.Effects
{
public class IosLifecycleEffect : PlatformEffect
{
private const NSKeyValueObservingOptions ObservingOptions = NSKeyValueObservingOptions.Initial | NSKeyValueObservingOptions.OldNew | NSKeyValueObservingOptions.Prior;
private ViewLifecycleEffect _viewLifecycleEffect;
private IDisposable _isLoadedObserverDisposable;
protected override void OnAttached()
{
_viewLifecycleEffect = Element.Effects.OfType<ViewLifecycleEffect>().FirstOrDefault();
UIView nativeView = Control ?? Container;
_isLoadedObserverDisposable = nativeView?.AddObserver("superview", ObservingOptions, IsViewLoadedObserver);
}
protected override void OnDetached()
{
_viewLifecycleEffect.RaiseUnloaded(Element);
_isLoadedObserverDisposable.Dispose();
}
private void IsViewLoadedObserver(NSObservedChange nsObservedChange)
{
if (!nsObservedChange.NewValue.Equals(NSNull.Null))
_viewLifecycleEffect?.RaiseLoaded(Element);
else if (!nsObservedChange.OldValue.Equals(NSNull.Null))
_viewLifecycleEffect?.RaiseUnloaded(Element);
}
}
}
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.
I've custom renderer in Xamarin and I wonder how to dynamically update its value.
Here is my control in the main class:
public class MainControl : View
{
public double A
{
get;
set;
}
}
Here is my custom renderer, defined in Android:
[assembly: Xamarin.Forms.ExportRenderer(typeof(MainApplication.MainControl), typeof(MainApplication.Droid.CustomRenderer))]
namespace MainApplication.Droid
{
public class CustomRenderer : ViewRenderer<MainControl,
MainApplication.Droid.ControlAndroid>
{
private ControlAndroid control;
public CustomRenderer(Context context) : base(context)
{
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
}
protected override void OnElementChanged(ElementChangedEventArgs<MainControl> e)
{
base.OnElementChanged(e);
if (Control == null)
{
control = new ControlAndroid(Context);
SetNativeControl(control);
}
}
}
}
The method OnElementChanged does only update when creating the object. OnElementPropertyChanged are not trigged.
I expected that something should be trigged when changing the value of the property A from the main class.
I found the answer by my own. I figured out that I needed a bindable property (connected to my regular property "A") in order to get a call on OnElementPropertyChanged.
I have a strange recurring problem. Sometimes it goes away, other times it comes back. I can't pinpoint at all the issue, all my breakpoints seem to be hit in expected order.
When I navigate to a new page, my backstack keeps getting deleted, so pressing back just backgrounds the app. Obviously this is a problem.
I think it may be a result of my more complex page and viewmodel structures. I created a new class for all the NavigationHelper stuff for Pages enforcing that all my Pages subclass from the new class. I enforce that all my Pages attach themselves to a base PageViewModel class to resolve the communication between the two (I had a better way but Xaml doesn't play well), and I navigate using a NavigationService, where I call CurrentFrame, which is a static method for return Windows.Current.Content as Frame.
Here are what I think are relevant code. Any ideas? Thanks a bunch in advance. I have no clue what's going on :/
I navigate forward using the Navigate method in NavigationService (not the other two lolol), but my back button doesn't go back properly.
public abstract class BaseViewModelPage : Page
{
protected readonly NavigationHelper NavigationHelper;
protected BaseViewModelPage()
{
NavigationHelper = new NavigationHelper(this);
NavigationHelper.LoadState += navigationHelper_LoadState;
NavigationHelper.SaveState += navigationHelper_SaveState;
this.NavigationCacheMode = NavigationCacheMode.Required;
}
protected BasePageViewModel CurrentPageViewModel
{
get { return DataContext as BasePageViewModel; }
}
#region Navigation Registration
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedFrom(e);
}
protected virtual void LoadState(LoadStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.LoadState(e);
}
}
protected virtual void SaveState(SaveStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.SaveState(e);
}
}
private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
LoadState(e);
}
private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
SaveState(e);
}
#endregion
}
public abstract class BasePageViewModel : ViewModelBase
{
private bool _isLoading = false;
public bool IsLoading
{
get
{
return _isLoading;
}
set
{
if (_isLoading == value)
{
return;
}
_isLoading = value;
RaisePropertyChanged();
}
}
public virtual void LoadState(LoadStateEventArgs e)
{
}
public virtual void SaveState(SaveStateEventArgs e)
{
}
}
public class NavigationService : INavigationService
{
public static readonly Dictionary<Type, Type> PageDictionary;
static NavigationService()
{
PageDictionary = new Dictionary<Type, Type>();
PageDictionary.Add(typeof(LogInPageViewModel), typeof(LogInPage));
PageDictionary.Add(typeof(RegisterUserPageViewModel), typeof(RegisterUserPage));
}
public bool Navigate(Type pageViewModelType, Object parameter = null)
{
if (PageDictionary.ContainsKey(pageViewModelType))
{
if (parameter != null)
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType], parameter);
}
else
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType]);
}
}
return false;
}
public bool GoBack()
{
if (CanGoBack())
{
App.CurrentFrame.GoBack();
}
return false;
}
public bool CanGoBack()
{
return App.CurrentFrame.CanGoBack;
}
public bool NavigateAndRemoveSelf(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
if (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
return true;
}
}
return false;
}
public bool NavigateAndRemoveAll(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
while (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
}
return true;
}
return false;
}
}
Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
and a second reliance on the Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add the WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!
Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
...
#endif
Because the MACRO wasn't defined, the back button wouldn't actually go back.
A second problem was the missing Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add a WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!
You can leave your NavigationHelper in your shared project, just add this to your MainPage in the Windows Phone project..
static MainPage()
{
HardwareButtons.BackPressed += (sender, args) =>
{
var frame = Window.Current.Content as Frame;
if (frame != null && frame.CanGoBack)
{
frame.GoBack();
args.Handled = true;
}
};
}
This solved my BackButton issues.
Summary
I'm experimenting with the MVP pattern in a Windows Forms application.
I'd like to make both my Presenters and Views platform agnostic, so if I wish to port my application to another platform, say the Web or mobile, I simply need to implement the views with platform dependant GUI, and my presenters can still be platform independent.
And now I wonder, how to ShowDialog() using MVP and Passive Views?
To my understanding so far, passive views shouldn't know/care about any presenter. They don't even know it exists. So, the solution presented in this question's answer is not suitable, according to me:
Refactoring Form.ShowDialog() code to MVP
Some code samples to help on the understanding:
ApplicationView
public partial class ApplicationForm : Form, IApplicationView {
// I ensure that all the IApplicationView events are raised
// upon button clicks text changed, etc.
// The presenter, which this view ignores the existence,
// is subscribed to the events this view raises.
}
ApplicationPresenter
public class ApplicationPresenter
: Presenter<IApplicationView>
, IApplicationPresenter {
public ApplicationPresenter(IApplicationView view) : base(view) {
View.OnViewShown += OnViewShown;
}
public void OnViewShown() {
IAuthenticaitonView authView = new AuthenticationForm();
IAuthenticationPresenter authPresenter = new AuthenticationPresenter(authView);
authPresenter.ShowDialog(); // 1.
}
}
This is where I'm struggling. The ApplicationPresenter is like the master in the universer and may be aware of the user authentication through both the IAuthenticationView and IAuthenticationPresenter.
IAuthenticationView
public interface IAuthenticationView : IDialogView {
string ErrorMessage { get; set; }
string Instance { get; set; }
IEnumerable<string> Instances { get; set; }
string Login { get; set; }
string Password {get; set; }
void EnableConnectButton(bool enabled);
event VoidEventHandler OnConnect;
event SelectionChangedEventHandler OnDatabaseInstanceChanged;
event VoidEventHandler OnLoginChanged;
event VoidEventHandler OnPasswordChanged;
}
IDialogView
public interface IDialogView : IView {
void ShowDialog();
}
IView
public interface IView {
void Show();
event VoidEventHandler OnViewInitialize;
event VoidEventHandler OnViewLoad;
event VoidEventHandler OnViewShown;
}
IAuthenticationPresenter
public interface IAuthenticationPresenter : IPresenter<IAuthenticationView> {
void OnConnect();
void OnViewDatabaseInstanceChanged(SelectionChangedEventArgs e);
void OnViewLoginChanged();
void OnViewPasswordChanged();
}
IPresenter<V>
public interface IPresenter<V> where V : IView {
V View { get; }
OnViewInitialize();
OnViewLoad();
ShowView();
}
Based on these premisses:
The presenter shall be platform agnostic
Only the view knows how to show/display itself (WPF, Mobile, Silverlight, Web, WinForms...)
The view MUST provide a way to show itself
The view doesn't have to be platform agnostic, since the display will differ from a platform to another
But the presenter shall order the view when to show itself
I came to this:
IView
public interface IView {
void OnShowView();
}
IPresenter<V>
public interface IPresenter<V>where V : IView {
void ShowView();
event VoidEventHandler OnShowView;
}
Presenter<V>
public abstract class Presenter<V> : IPresenter<V> {
public Presenter(V view) {
View = view;
OnShowView += View.OnShowView;
}
public void ShowView() { raiseShowViewEvent(); }
public event VoidEventHandler OnShowView;
private void raiseShowViewEvent() { if (OnShowView != null) OnShowView(); }
}
So, following the logic of where I struggled so far, I solved it by doing this:
ApplicationForm
public partial class ApplicationForm : Form, IApplicationView {
private void ApplicationForm_Shown(object sender, EventArgs e) { raiseOnViewShown(); }
private void raiseOnViewShownEvent() { if (OnViewShown != null) OnViewShown(); }
}
ApplicationPresenter
public void OnViewShown() {
// This method is the subscriber of the IView.OnViewShown event
// The event is raised with the ApplicationForm_Shown event.
IAuthenticationView authView = new AuthenticationForm();
IAuthenticationPresenter authPresenter = new AuthenticationPresenter(authView);
authPresenter.ShowView(); // 1.
}
This raises the OnShowView event which the IAuthenticationView has subscribed. Then, back in the form, the view's response to the event is:
AuthenticationForm
public partial class AuthenticationForm : Form, IAuthenticationView {
public void OnShowView() { ShowDialog(); }
}
Then, the view shows itself as a dialog/modal window.