Bind Two Different Views for same ViewModel - xamarin

This looks like similar to MVVMCross - Bind the same ViewModel to 2 different Views
But after there I don't get an answer how can I select one View for ViewModel and in another time the second View for the same ViewModel.
As an example, I wanna have one ViewModel - LoginViewModel and two Views: PhoneLoginPage & TabletLoginPage.
According to information from Xamarin.Forms.Device.Idiom when it's a Phone I want to show PhoneLoginPage but when it's a Tablet - TabletLoginPage but have the same LoginViewModel binded to them.
How can I achieve it correctly? Mean, without any dirty tricks...
Thanks

NOTE: I'm also on the side of having only one page.
But here's how I'd go about having two pages:
In case you're using some kind of a navigation service:
Just register different pages on app startup based on the device's idiom.
Along the lines of:
if(Device.Idiom == TargetIdiom.Tablet)
{
SomeNavigationService.Register<LoginViewModel, TabletLoginPage>();
}
else
{
SomeNavigationService.Register<LoginViewModel, PhoneLoginPage>();
}
In case you're doing the navigation in the page code-behind
Choose the page based on the device's idiom there...
if(Device.Idiom == TargetIdiom.Tablet)
{
await Navigation.PushAsync(new TabletLoginPage(viewModel));
}
else
{
await Navigation.PushAsync(new PhoneLoginPage(viewModel));
}

With MvvmCross this is not possible, in runtime, we will get an Exception if we create two Views for the same ViewModel.
My solution was next:
<ProjectFolder>
=> Pages
=> Mobile
XMobilePage.xaml
YMobilePage.xaml
=> Tablets
XTabletPage.xaml
YTabletPage.xaml
=> PageModels
=> Mobile
XMobilePageModel.cs
YMobilePageModel.cs
=> Tablets
XTabletPageModel.cs
YTabletPageModel.cs
class XMobilePage: MvxContentPage<XMobilePageModel> {...}
class XTabletPage: MvxContentPage<XTabletPageModel> {...}
abstract SharedXPageModel
{
//All Shared Commands and Comand Logic and other shared initialization
abstract Task NavigateToYPageModel();
}
class XMobilePageModel : SharedXPageModel
{
Task NavigateToYPageModel() => _mvxNavigationService.Navigate<YMobilePageModel>();
}
class XTabletPageModel : SharedXPageModel
{
Task NavigateToYPageModel() => _mvxNavigationService.Navigate<YTabletPageModel>();
}

Related

ViewModel not resolved

I have a UWP Application using ReactiveUI. I navigate to a page with this code:
Router.Navigate.Execute(new AccountListViewModel(this));
The navigation is done. But the ViewModel I created for the navigation is not assigned to my ViewModel in the View. IViewFor<> is implemented as follows:
public sealed partial class AccountListView : IViewFor<AccountListViewModel>
{
public static readonly DependencyProperty ViewModelProperty = DependencyProperty
.Register(nameof(ViewModel), typeof(AccountListViewModel), typeof(AccountListView), null);
public AccountListView()
{
this.InitializeComponent();
this.WhenActivated(disposables =>
{
// My Bindings
...
});
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (AccountListViewModel) value;
}
public AccountListViewModel ViewModel {
get => (AccountListViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
Or do I get something completly wrong here?
According to ReactiveUI RoutedViewHost implementation for Windows, which is used for Universal Windows Platform and for Windows Presentation Foundation, the view model should definitely get assigned to the IViewFor.ViewModel property. You can track changes in the IScreen.Router.CurrentViewModel property to make sure it changes.
If it does, make sure you properly bind your IScreen.Router property to the Router property of the UWP-specific RoutedViewHost XAML control, and routing should finally work. In fact, I tested that behavior on UWP recently and it worked fine for me with ReactiveUI 9.13.1 and latest UWP SDK. Try following the routing tutorial to fully understand how routing works. If this still won't work for you, then uploading a minimal repro that compiles to GitHub could help us understand your issue better. Also, come join ReactiveUI Slack, we are always ready to help out.

In MVVMCross 5, how can I navigate backward multiple pages?

In this example, a unique Person is defined by their FirstName and LastName. PageA is a form that selects a unique Person. PageB is a list of unique FirstNames, and PageC is a list of all the LastNames that exist for a given FirstName.
I'm having a hard time solving a particular UX pattern using MvxNavigationService. Here's what I'm attempting to do, (psuedocode):
PageA.SelectedItem = NavigateTo(PageB) [list of Person, grouped
by Person.FirstName];
PageB.SelectedItem = NavigateTo(PageC) [for
Person.FirstName, list of Person.LastName, ];
PageC.Close(SelectedItem);
PageB.Close(SelectedItem);
When I actually try and implement this and run it on Android, the viewmodel logic executes, but the UI doesn't show PageA.
Update: Calling PageB.Close() navigates back to PageC, since PageC was the previous page. Perhaps the problem could be solved by ensuring that PageC is removed from the stack upon closing it. How might this be accomplished?
There's many ways of doing this, using a Custom ViewPresenter on iOS or using an Activity on Android.
One way I achieved this for a small pair (2 view models) was by adding an Instance static variable to the first ViewModel that opens the second ViewModel, like this:
public class FirstViewModel
{
public static FirstViewModel Instance;
public void FirstViewModel()
{
Instance = this;
...
}
}
And then in the second ViewModel's save/close command, I just closed both ViewModels like this and it worked:
public new MvvmCross.Commands.IMvxCommand SaveClickCommand
{
get
{
return new MvvmCross.Commands.MvxAsyncCommand(
async () =>
{
await Navigator.Close(this);
await Navigator.Close(FirstViewModel.Instance);
}
);
}
}

Xamarin Forms Navigation without animation

I have an app where I want to show page A, from which the user can navigate to page B or C, from B back to A or to C, and from C only back to A, even if the user when through B to get to C
Currently when I'm executing the B->C transition I first PopAsync to get back to A and then I do PushAsync to get to C, so that the '
The question is: is there a civilized way to set up this navigation scheme while still relying on the built-in Navigation to keep track of navigation stack - I don't want to do that myself and use PushModalAsync.
Note that (as reflected in the image) A and C aren't the end points of the whole navigation stack, there are pages before A and after C, so the stack has to be preserved.
On iOS the NavigationRenderer has virtual methods OnPopViewAsync and OnPushAsync (similar on Android):
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, animated);
}
They call the corresponding base method with two arguments, the page and whether to animate the transition. Thus, you might be able to enable or disable the animation using the following approach:
Derive a custom navigation page.
Add an "Animated" property.
Derive a custom navigation renderer for your custom navigation page.
Override the pop and push methods calling their base methods with the "Animated" property.
Note that I haven't tried this approach, yet, since it is quite some work to do. But disabling animations on all navigation pages did work this way.
Edit: It took me several hours to actually implement my solution for my own project. Therefore, I'll share some more details. (I developed and tested on Xamarin.Forms 1.2.3-pre4.)
The custom navigation page
Besides the above-mentioned Animated property my navigation page re-implements the two transition functions and adds an optional argument animated, which is true by default. This way we'll be able to keep all existing code and only add a false where needed.
Furthermore, both method will sleep for a very short time (10 ms) after pushing/popping the page. Without this delay we'd ran into trouble with consecutive calls.
public class CustomNavigationPage: NavigationPage
{
public bool Animated { get; private set; }
public CustomNavigationPage(Page page) : base(page)
{
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task PushAsync(Page page, bool animated = true)
{
Animated = animated;
await base.PushAsync(page);
await Task.Run(delegate {
Thread.Sleep(10);
});
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task<Page> PopAsync(bool animated = true)
{
Animated = animated;
var task = await base.PopAsync();
await Task.Run(delegate {
Thread.Sleep(10);
});
return task;
}
}
The custom navigation renderer
The renderer for my custom navigation page overrides both transition methods and passes the Animated property to their base methods. (It's kind of ugly to inject a flag this way, but I couldn't find a better solution.)
public class CustomNavigationRenderer: NavigationRenderer
{
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, (Element as CustomNavigationPage).Animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, (Element as CustomNavigationPage).Animated);
}
}
This is for iOS. But on Android it's almost identically.
An example application
To demonstrate the possibilities of consecutively pushing and popping pages, I wrote the following application.
The App class simply creates a new DemoPage wrapped into a CustomNavigationPage. Note that this instance must be publicly accessible for this example.
public static class App
{
public static CustomNavigationPage NavigationPage;
public static Page GetMainPage()
{
return NavigationPage = new CustomNavigationPage(new DemoPage("Root"));
}
}
The demo page contains a number of buttons that push and pop pages in different orders. You can add or remove the false option for each call to PushAsync or PopAsync.
public class DemoPage: ContentPage
{
public DemoPage(string title)
{
Title = title;
Content = new StackLayout {
Children = {
new Button {
Text = "Push",
Command = new Command(o => App.NavigationPage.PushAsync(new DemoPage("Pushed"))),
},
new Button {
Text = "Pop",
Command = new Command(o => App.NavigationPage.PopAsync()),
},
new Button {
Text = "Push + Pop",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (will pop immediately)"));
await App.NavigationPage.PopAsync();
}),
},
new Button {
Text = "Pop + Push",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PushAsync(new DemoPage("Popped and pushed immediately"));
}),
},
new Button {
Text = "Push twice",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (1/2)"), false);
await App.NavigationPage.PushAsync(new DemoPage("Pushed (2/2)"));
}),
},
new Button {
Text = "Pop twice",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PopAsync();
}),
},
},
};
}
}
Important hint: It cost me hours of debugging to find out that you need to use an instance of NavigationPage (or a derivative) rather than the ContentPage's Navigation! Otherwise the immediate call of two or more pops or pushes leads to strange behaviour and crashes.
#Falko
You now have the possibility to include a boolean parameter:
Navigation.PushAsync (new Page2Xaml (), false);
Xamarin Documentation
Currently the Xamarin Forms navigation is very spartanic and I doubt there is a nice way to achieve that. Besides Doing and extra "Pop" when necessary.
Here's a collection of snippets I whipped together along with some other niceties to improve NaviagationPage for iOS. Link to comment and code on xamarin forums.
What I would do if I were doing this is push Page C on to your NavigationStack and then take page B off of the stack. That way when you pop from page C, you would go to page A.
// Push the page you want to go to on top of the stack.
await NavigationPage.PushAsync(new CPage()));
// Remove page B from the stack, so when you want to go back next time
//you will go to page A.
Navigation.RemovePage(Navigation.NavigationStack[Navigation.NavigationStack.Count - 2] );
Alternatively, when even you pop from page C, you could remove all instances of type page B from the stack, and then pop back 1. In that case, page B would remain on the stack until you were about to move back from page C to page A.

Localization and binding don't work together

I'm developing my first app and I'm trying to make it multilanguage.
Using AppHub example and some other link I created my resource files, fixed binding strings on my components and set a settings page.
First problem I had was that menu items and appbar buttons couldn't use localization strings (project complained when launched) so I have:
TextBlocks and other components binded with localized strings
Appbar buttons and items localized manually with a procedure loading localized strings
Now that I have my settings page, one item user can change is language.
Well, correct CultureInfo is selected according to user selection and then I use
Thread.CurrentThread.CurrentUICulture = Settings.Language;
When I press back button and return to main page, appbar items are localized correctly, while everything else is not.
The only workaround (that I really don't like, it's just to understand) is this:
public MainPage()
{
Thread.CurrentThread.CurrentUICulture = Settings.Language;
InitializeComponent();
// Everything else I need here
}
so I have to set language before components are created to make it work.
What's wrong? Which is the correct way to make a page refresh after changing language using binded strings?
I did not put a lot of code because I used basically the one provided in the link, but if you need more info I will edit my question.
I finally found a solution to automatically update my application components reacting to language change.
A good tutorial can be found here; briefly you must find a way to notify your app that localized resource is changed.
public class LocalizedStrings : ViewModelBase
{
private static AppResources localizedresources = new AppResources();
public AppResources LocalizedResources
{
get { return localizedresources; }
}
public void UpdateLanguage()
{
localizedresources = new AppResources();
RaisePropertyChanged(() => LocalizedResources);
}
public static LocalizedStrings LocalizedStringsResource
{
get
{
return Application.Current.Resources["LocalizedStrings"]
as LocalizedStrings;
}
}
}
With this when user change language, you should simply run
LocalizedStrings.LocalizedStringsResource.UpdateLanguage();
and the job is done.

MvvmCross - Sharing viewmodels for multiple views

I have been using MvvmCross on a cross platform mobile project and have 2 different views in a MonoTouch project that are using the same shared viewmodel and not sure how to go about structuring my code to navigate to different views using the same viewmodel in MvvmCross.
The default convention used by the MvvmCross platform is to automatically register all views using reflection.
This is done in the base Setup class - in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Platform/MvxBaseSetup.cs:
protected virtual void InitializeViews()
{
var container = this.GetService<IMvxViewsContainer>();
foreach (var pair in GetViewModelViewLookup())
{
Add(container, pair.Key, pair.Value);
}
}
where GetViewModelViewLookup returns a dictionary of ViewModel type to View type:
protected virtual IDictionary<Type, Type> GetViewModelViewLookup(Assembly assembly, Type expectedInterfaceType)
{
var views = from type in assembly.GetTypes()
let viewModelType = GetViewModelTypeMappingIfPresent(type, expectedInterfaceType)
where viewModelType != null
select new { type, viewModelType };
return views.ToDictionary(x => x.viewModelType, x => x.type);
}
In universal iPad/iPhone apps you do occasionally want to include multiple views for each viewmodel - using one view in the iPad and one view in the iPhone.
To do this, there are now (literally just now!) some attributes available to mark your views as being "unconventional" - these are:
MvxUnconventionalViewAttribute
use this to mark that your view should never be included by convention
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Views/Attributes/MvxUnconventionalViewAttribute.cs
MvxConditionalConventionalViewAttribute
an abstract attribute - override this to provide your own custom logic for inclusion/exclusion
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Views/Attributes/MvxConditionalConventionalViewAttribute.cs
MvxFormFactorSpecificViewAttribute
iOS/Touch only
an attribute that will include the view if and only if the detected iPhone form factor matches the current device
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Touch/Views/Attributes/MvxFormFactorSpecificViewAttribute.cs
The last of these is probably what you want in this case - you could implement simple iPhone/iPad switching for a MainViewModel using two views declared like:
[MvxFormFactorSpecificView(MvxTouchFormFactor.Phone)]
public class MyIPhoneView : BaseView<MainViewModel>
{
// iphone specific view ...
}
[MvxFormFactorSpecificView(MvxTouchFormFactor.Pad)]
public class MyIPadView : BaseView<MainViewModel>
{
// ipad specific view ...
}
Alternatively if you want a very custom configuration, you can override all 'convention-based' behaviour - you can implement your own override of GetViewModelViewLookup - e.g.:
protected override IDictionary<Type, Type> GetViewModelViewLookup(Assembly assembly, Type expectedInterfaceType)
{
if (IsIPad)
{
return new Dictionary<Type, Type>()
{
{ typeof(HomeViewModel), typeof(IPadHomeView) },
{ typeof(DetailViewModel), typeof(IPadDetailView) },
{ typeof(AboutViewModel), typeof(SharedAboutView) },
};
}
else
{
return new Dictionary<Type, Type>()
{
{ typeof(HomeViewModel), typeof(IPhoneHomeView) },
{ typeof(DetailViewModel), typeof(IPhoneDetailView) },
{ typeof(AboutViewModel), typeof(SharedAboutView) },
};
}
}
Note that eventually you may decide that you need additional ViewModels as well as Views for the iPad app - the iPad has, after all, a much bigger screen - in this case you can add them manually. Ultimately, when your app hits a few million users, you may even decide to completely branch the tablet code away from the phone code - but that can generally wait until you hit that few million mark...
Another way to do it is to go ahead and create 2 ViewModels, but have them both subclass an abstract ViewModel, as follows:
FirstViewViewModel : BaseViewModel
SecondViewViewModel : BaseViewModel
With the corresponding views named:
FirstView.xaml
SecondView.xaml
This way, you are able to place some shared behavior in BaseViewModel, while the 2 subclasses are really just there to satisfy MvvmCross' view fetching conventions.
I recently started with MvvmCross and I am using v4.2.1. It seems that some names have changed. I am using one ViewModel with seperate iPhone and iPad views with the following:
[MvxFormFactorSpecific(MvxIosFormFactor.Phone)]
public class MyIPhoneView : BaseView<MainViewModel>
{
// iphone specific view ...
}
[MvxFormFactorSpecific(MvxIosFormFactor.TallPhone)]
public class MyTallIPhoneView : BaseView<MainViewModel>
{
// tall iphone specific view ...
}
[MvxFormFactorSpecific(MvxIosFormFactor.Pad)]
public class MyIPadView : BaseView<MainViewModel>
{
// ipad specific view ...
}

Resources