First things first – Why?
First of all I do not like how creating of views and navigation works in Xamarin.
There is much boilerplate needed to just navigate from one page to another.
Typically like in Xamarin documentation we have to obtain INavigation
interface instance, create instance of a new page, create instance of view model and all dependencies it requires.
When you page do not require view model things are real easy and Xamarin sample is just enough.
await Navigation.PushAsync(new LoginPage());
Right?! Easy! You want to use this framework because it so intuitive! 🙂
When your page do not require sophisticated initialization nor view model have dependencies things are easy too.
await Navigation.PushAsync(new LoginPage(){ BindingContext = new LoginViewModel() });
Yes, it is possible to create view model instance via Xaml of course.
<Page.BindingContext> <LoginViewModel /> </Page.BindingContext>
And this is why you should never do that:
- Xaml intellisense is quite poor currently (as far as I know because I am using R#, which takes care of most of that)
- It is possible to write bad Xaml and you will NOT have any compile-time errors (and you are using strongly typed language, C# just for that, to get that kind of feedback). Those errors can be caught much later, in test or if view is rarely used, it can even travel to end user.
- Since you will not get any errors, you will have to specifically remember to add parameters, or other type of view model initialization. Did you ever tried to add constructor parameters to Xaml? It is possible, but complicated and I do not understand why would anyone tried that if this can be done easier and safer in code.
We should do it in code then.
When your view model have some dependencies (only 2 is not that much really) things becomes looks ugly.
await Navigation.PushAsync(new LoginPage() { BindingContext = new LoginViewModel(){ AuthorizationService = new AutorizationService(new WebService(), new DataBase()); }; } );
What if you navigate to the same page from more than one place? Do you create static method? It is viable solution to move view model creation to view constructor for example. Then, you could write only one line, like in example from Xamarin documentation, each time you want to navigate to this page. But still, if you have set of services and some of them are singletons, you will have to obtain instance of each of them in view or view model (much better) and change of dependencies will require few lines more. Lines that you probably wrote someplace else already. If you are lazy, like me, you probably do not want to repeat yourself. 🙂
Of course in official documentation in samples INavigation instance is taken from VisualElement.Navigation, but if you need to pass some parameters between pages (simplest example is to pass customer instance from previous page with list of customers, to show details of this customer in next one) this solution becomes problematic. If you bind some data to view model and want to pass it to next one, you will need to obtain that data by casting BindingContext to appropriate type, create next view model instance and set required data to it.
await Navigation.PushAsync(new MainPage() { BindingContext = new MainViewModel(){ User = ((LoginViewModel)BindingContext).Login, DataBase = new DataBase(), GPSService = new GPSService(), CameraService = new CameraService(), }; } );
This is just to show problems with navigation implemented like that.
There is really simple solution to dependencies problem. It is widely used and called IoC. With container, you can create view models and set dependencies via constructor or properties. It also perfectly doable to use IoC container to obtain view model dependencies in view or in view model constructor. But again it is bad practice. IoC container should be (in ideal world) used once, for example to create first view and then work in background. Having that in mind view is not best place to use it, since Xaml requirement is, that views do not have parameters in constructors. View model it is then.
What would be better
Best way to navigate between views in view models is in my opinion one line with one method
Navigate<MainViewModel>();
And if you need special initialization or passed parameters from previous view model:
Navigate<MainViewModel>((mvm)=>mvm.User = Login);
Would you not agree, that this looks much nicer?
But how to do that? Of course best solution is for Navigate method to do all the initialization of view and view model, setting BindingContext to view model and pushing it to INavigation instance. Overload with action would do extra initialization of new view model.
- Obtain correct view for given view model type.
- Create view instance.
- Create view model instance and get all its dependencies from IoC.
- Set view model in view
- Set new view to Master page if necessary.
- Obtain INavigation instance and push newly created view on top of that.
In this article we will cover first two points, fourth and partially third one. If you are wondering what Master page is all about, you should read my previous article. In summary: we want seamless views creation and navigation between them, regardless if there are only detail views (with menu, top bar, bottom status bar etc.) or whole screen views (without menu, top bar, bottom status bar etc.). Rest will be covered in next article.
Implementation of ViewFactory
To ease our start with new application we can reuse some code from Master page article. This code is available on github.
To show creation of ordinary pages and detail pages, main application need only two menu items, two views and application file. We can start by creating ViewFactory shared xamarin project and add files from previous article, with just little changes.
File App.xaml have almost the same content as before.
<?xml version="1.0" encoding="utf-8" ?> <Application x_Class="ViewFactory.App"> </Application>
Code file App.xaml.cs is very similar too.
using CustomMasterDetailControl; namespace ViewFactory { public partial class App { public App() { InitializeComponent(); MainPage = MasterDetailControl.Create<MasterDetail, MasterDetailViewModel>(); } } }
Detail.xaml and MasterDetail.xaml are almost the same as Detail1.xaml and MasterDetail from previous article, respectively.
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ViewFactory.Detail"> <Label Text="Detail" VerticalOptions="Center" HorizontalOptions="Center" /> </customMasterDetailControl:DetailPage>
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ViewFactory.Detail"> <Label Text="Detail" VerticalOptions="Center" HorizontalOptions="Center" /> </customMasterDetailControl:DetailPage>
MasterDetailViewModel have only two commands to navigate to detail page and ordinary page. For now it just creates and set both pages as detail views, but we will change that after implementing ViewFactory class.
using System.Windows.Input; using CustomMasterDetailControl; using Xamarin.Forms; namespace ViewFactory { public class MasterDetailViewModel : MasterDetailControlViewModel { private ICommand _toDetai; private ICommand _toOrdinaryPage; public ICommand ToDetail { get { return _toDetai ?? (_toDetai = new Command(OnToDetail)); } } public ICommand ToOrdinaryPage { get { return _toOrdinaryPage ?? (_toOrdinaryPage = new Command(OnToOrdinaryPage)); } } private void OnToOrdinaryPage() { Detail = new OrdinaryPage(); } private void OnToDetail() { Detail = new Detail(); } } }
OrdinaryPage derives from new UIPage class which serve as base class for DetailPage (base class of Detail page in Detail.xaml file). This way we can use one single base class for all of our views. It makes ViewFactory class a little simpler, but if it is impossible in your project, it will be simple enough to make this work with different one or even more than one base class for both types of views.
And OrdinaryPage also just have a single label inside.
<?xml version="1.0" encoding="utf-8" ?> <masterDetail:UIPage x_Class="ViewFactory.OrdinaryPage"> <Label Text="Ordinary page" VerticalOptions="Center" HorizontalOptions="Center" /> </masterDetail:UIPage>
One last touch to make this first version work: we need to copy and change a little CustomMasterDetailControl project.
BaseViewModel class is exactly what it seems – it only have implementation of INotifyPropertyChanged interface and we will need this base class for all of our view models. It is used in MasterDetailControlViewModel class instead of standalone implementation.
using System.ComponentModel; using System.Runtime.CompilerServices; namespace CustomMasterDetailControl { public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
MasterDetailControlViewModel class now derives from BaseViewModel.
public class MasterDetailControlViewModel : BaseViewModel, INavigation
UIPage as mentioned above will serve as base class for all views, both partial and for whole screen:
using Xamarin.Forms; namespace CustomMasterDetailControl { public class UIPage : ContentPage { } }
namespace CustomMasterDetailControl { public class DetailPage : UIPage { public DetailPage() { SideContentVisible = true; } public bool SideContentVisible { get; set; } } }
Ok. Now we can implement ViewFactory class. First thing to do is to create relations between view model types and view types. To do that, we need to scan all types and find all views. We can do it in Init method of ViewFactory class.
public static void Init() { var appAssebly = Assembly.GetExecutingAssembly(); var pages = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<UIPage>()); var viewModels = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<BaseViewModel>()).ToArray(); foreach (var page in pages) { var viewModel = viewModels.FirstOrDefault(vm => vm.Name == page.Name + "Model"); if (viewModel != null) { Views.Add(viewModel, new ViewData { IsDetail = page.IfHaveBaseClassOf<DetailPage>(), ViewModelType = viewModel, ViewType = page }); } } }
ViewData class is just really simple data object.
public class ViewData { public Type ViewType { get; set; } public Type ViewModelType { get; set; } public bool IsDetail { get; set; } }
What is IfHaveBaseClassOf method? It is just simple Type type extension method. Contains logic for searching all base types in inheritance tree, to check if any of them is the one we are looking for.
public static bool IfHaveBaseClassOf(this Type type) where TBase : class { var haveBase = false; var typeToCheck = type; var baseType = typeof(TBase); while (typeToCheck != null) { if (typeToCheck.BaseType == baseType) { haveBase = true; break; } else { typeToCheck = typeToCheck.BaseType; } } return haveBase; }
It takes some type and checks if any base type in tree of inheritance is of specified type we want to find. It allows us to go through every type in assembly and check if it derive from BaseViewModel or UIPage classes which both are needed for our ViewFactory. Oh, and there is a check if view is detail view (derive from DetailPage) also with this method.
With relations between views and view models we can implement method for creating views.
public UIPage CreateView<TViewModel>() where TViewModel : BaseViewModel { return CreateView(typeof(TViewModel)); } public UIPage CreateView(Type viewModelType) { if (Views.ContainsKey(viewModelType)) { var viewData = Views[viewModelType]; var page = (UIPage)Activator.CreateInstance(viewData.ViewType); var viewModel = Activator.CreateInstance(viewModelType); page.BindingContext = viewModel; return page; } return null; }
As you can see it is really simple. It takes either type parameter or parameter with view model type. Then it find view type and instantiate both via Activator class. With view model it can became somewhat problematic if it does have dependencies, but it can easily be changed with IoC container, which would resolve view model with dependencies. Last thing it does, is setting view model as binding context for view.
This is just enough to have first working application with this solution. With just small change to MasterDetailViewModel commands we can now navigate via view models instead of views.
public class MasterDetailViewModel : MasterDetailControlViewModel { private readonly ViewFactory.ViewFactory _viewFactory; private ICommand _toDetai; private ICommand _toOrdinaryPage; public MasterDetailViewModel() { _viewFactory = new ViewFactory.ViewFactory(); } public ICommand ToDetail { get { return _toDetai ?? (_toDetai = new Command(OnToDetail)); } } public ICommand ToOrdinaryPage { get { return _toOrdinaryPage ?? (_toOrdinaryPage = new Command(OnToOrdinaryPage)); } } private void OnToDetail() { Detail = _viewFactory.CreateView<DetailPage>(); } private void OnToOrdinaryPage() { Navigation.PushAsync(_viewFactory.CreateView<OrdinaryPage>()); } }
After starting this application we will see something similar to this:
First button will take us to detail page (menu still will be visible) and second one will take us to page for whole screen (without menu).
Things to improve
But there are still few points for improvements:
- Faster type instantiation instead of Activator
- Use of views and view models from libraries
- Some views might need specific view model type
- We can disable reading of all or only specific assemblies
Faster Views creation
Instead of using Activator.CreateInstence it is a bit faster (according to this post on StackOverflow) to use Expression.Lambda. My tests proves that it is true and compiled expression is about 25% faster than Activator.CreateInstance method. To use that technic we need to create delegate for each view. It is also possible for view model but it would not be good idea for constructors with parameters (which is something we want to do but with IoC). Let us change a little Init method and ViewData class. In ViewData we just have to add property for creator delegate:
public Func Creator { get; set; }
We need to fill this property in Init method:
Creator = (Func<UIPage>)Expression.Lambda(Expression.New(page.GetConstructor(Type.EmptyTypes))).Compile(),
It is not completely safe code, since Expression.New method expects not null parameter and Type.GetConstructor method can return null, if there is no constructor specified with given parameters (in this particular case without parameters). However this will happed only when view do not have default parameterless constructor, which view should have anyway. Anyway, in such a case Activator.CreateInstance would throw error too.
External libraries
Sometimes we have views and view models in different assembly than application assembly. Obviously, we want to be able to navigate to those views too. It is very easy to handle them with small change to our code. We only need to iterate through all of application assemblies and scan them as we do for application assembly. For this we can extract this scanning logic to new ScanAssembly method:
private static void ScanAssembly(Assembly appAssebly) { var pages = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<UIPage>()); var viewModels = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<BaseViewModel>()).ToArray(); foreach (var page in pages) { var viewModel = viewModels.FirstOrDefault(vm => vm.Name == page.Name + "Model"); if (viewModel != null) { Views.Add(viewModel, new ViewData { IsDetail = page.IfHaveBaseClassOf<DetailPage>(), ViewModelType = viewModel, ViewType = page, Creator = (Func<UIPage>)Expression.Lambda(Expression.New(page.GetConstructor(Type.EmptyTypes))).Compile(), }); } } }
And we just need to iterate through all of assemblies in Init method.
var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { ScanAssembly(assembly); }
With this our ViewFactory will register all views for available view models in all of assemblies.
Overriding external views
But sometimes, if you have some library with predefined views and view models for them (for login page i.e.), you do not want to use default view and instead use other one that will be more suitable for application. It would be nice to be able to override those default views. Best way to do that, would be to create view with the same name in application assembly and let ViewFactory do the rest – scanning and overriding the default.
First, we have to modify a little our scanning method.
private static void ScanAssembly(Assembly appAssebly, bool @override = false) { var pages = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<UIPage>()); var viewModels = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<BaseViewModel>()).ToArray(); foreach (var page in pages) { var viewModel = viewModels.FirstOrDefault(vm => vm.Name == page.Name + "Model"); if (viewModel != null) { if (!Views.ContainsKey(viewModel)) { Views.Add(viewModel, GetViewData(viewModel, page)); } } //no view model in assembly and if app assembly -> override else if (@override) { var viewToOverride = Views.FirstOrDefault(kv => kv.Key.Name == page.Name + "Model"); if (viewToOverride.Key != null) { Views[viewToOverride.Key] = GetViewData(viewToOverride.Value.ViewModelType, page); } } } }
New else clause do exactly that. If view model is not found in assembly, and this assembly is main application assembly (override parameter is equal true), ViewFactory looks through its current registration list and override if there is already view model with name similar to view name.
Before last improvement (scanning only specific assemblies), we can test other, new features.
We can create new PCL project named ExternalAssembly with ExternalView, ToOverrideView and appropriate view models for them.
In ExternalView and ToOverrideView files we have following content:
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ExternalAssembly.ExternalView"> <Label Text="External page" VerticalOptions="Center" HorizontalOptions="Center" FontSize="40"/> </customMasterDetailControl:DetailPage>
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ExternalAssembly.ToOverrideView"> <Label Text="Not overriden view" VerticalOptions="Center" HorizontalOptions="Center" FontSize="40"/> </customMasterDetailControl:DetailPage>
Nothing really fancy – just a big labels with simple text, which is just enough to see if this works. View models for those views are just classes that derives from BaseViewModel.
For ViewFactory, to be able to see new project, we need to add reference to ViewFactory.Droid project. Also we need to add buttons to navigate to those new views and commands to handle them:
<Button Text="To external page" Command="{Binding ToExternalPage}" /> <Button Text="To overriden page" Command="{Binding ToOverridenPage}" />
private ICommand _toExternalPage; private ICommand _toOverridenPage; public ICommand ToExternalPage { get { return _toExternalPage ?? (_toExternalPage = new Command(OnToExternal)); } } public ICommand ToOverridenPage { get { return _toOverridenPage ?? (_toOverridenPage = new Command(OnToOverriden)); } } private void OnToExternal() { Detail = _viewFactory.CreateView<ExternalViewModel>(); } private void OnToOverriden() { Detail = _viewFactory.CreateView<ToOverrideViewModel>(); }
After running this code and trying new buttons we will see something similar to following screenshots.
Now we should try to override ToOverrideView. To do that we need to create view with the same name in ViewFactory.Droid assembly.
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ViewFactory.ToOverrideView"> <Label Text="Overriden view" VerticalOptions="Center" HorizontalOptions="Center" FontSize="40" /> </customMasterDetailControl:DetailPage>
It is almost the same as previous one with different label text. We just need to start an application and all the magic will happen automatically :). After clicking on last button (To overriden page) we will see different label this time.
We do not need all assemblies
Another point for improvements will be to disable scanning of all application assemblies. We don’t need to scan System.Xml and other .NET dlls. One of the solutions would be to ignore those assemblies. It would not be hard to just skip them in ViewFactory, but still there are many libraries and Nuget packages that do not have views and they would be scanned anyway. To be true, there are much more assemblies without views in application than with views. Logical solution is then to look for assemblies with them to be scanned later. For example it is possible to add assembly attribute to mark assemblies with views for scanning. Simple one as below is enough.
using System; namespace ViewFactory { [AttributeUsage(AttributeTargets.Assembly)] public class ViewAssemblyAttribute : Attribute { } }
But there is one problem. Since we need to mark i.e. ExternalAssembly as assembly with views we need this attribute to be accessible in this project. So we need third project with this attribute so we can reference it in ExternalAssembly. Even then new project with just a single attribute do not makes sense. Best thing would be to create new project with ViewFactory and other types it needs.
Unfortunately it is not that easy. Ideally it would be to place everything in PCL project, but portable version of .NET is very limited. There is no way to get list of assemblies, executing assembly, even acquiring base type of type is more complicated. There are two solutions we can use to get around those limitations.
- Split everything into PCL project and platform specific projects (with full .NET). Minus, big minus, of this solution is that you need to implement everything that requires full .NET more than once. But if you will write bigger framework for your applications you will do that at some point anyway, and if you have set of assemblies like that, you can do that very easily. IoC also makes this easier since you can create services for every specific part of .NET support that do not exists in PCL version (like IAdvancedReflection for part of reflection support that exists only in full .NET)
- Or you can try to remedy lacks of some properties or methods by combination of reflection, delegates, and expressions.
Since the first solution is really boring (and more easy, safe, testable etc.) we will cover second one first :). On aside note, it makes sense to use it only for single properties or methods. Writing bigger parts functionality like this would be really hard.
Remember TypesExtension class with single method IfHaveBaseClassOf? It wont work in PCL, without changes because portable .NET lacks Type.BaseType property. Oh, there is BaseType property, but on TypeInfo type. And TypeInfo have AsType method that returns corresponding Type object. But BaseType property returns Type value and there is need to cast it to TypeInfo again.
public static bool IfHaveBaseClassOf<TBase>(this Type type) where TBase : class { var haveBase = false; var typeToCheck = type.GetTypeInfo(); var baseType = typeof(TBase); while (typeToCheck != null) { if (typeToCheck.BaseType == baseType) { haveBase = true; break; } typeToCheck = typeToCheck.BaseType.GetTypeInfo(); } return haveBase; }
Ok, this is just juggling Type and TypeInfo, even if Type have BaseType property which is just unavailable in PCL. We could use reflection to get this property value.
var baseType = (Type)typeof(Type).GetRuntimeProperty("BaseType").GetValue(type);
But whole point of moving this to separate project was to make things faster, by reducing list of assemblies! This way, certainly, it would not become any faster. How we make it be better, then? We can make ourselves function to retrieve BaseType, the same way as we did with views constructors. There is even Expression.Property method just for that.
var parameterExpression = Expression.Parameter(typeof(Type)); GetBaseTypeFunc = (Func<Type, Type>)Expression.Lambda(Expression.Property(parameterExpression, "BaseType"), parameterExpression).Compile();
This creates delegate of function that takes Type object and returns its property BaseType. Parameter expression is used twice, because it is mentioned twice in lambda: first as parameter declaration and the as use. This may be a little confusing (at least was for me at first) so this is how this would be defined as ‘usual’ lambda.
(Type type)=>type.BaseType
According to my tests it is quicker than using portable .NET TypeInfo.AsType(), Type.GetTypeInfo() and TypeInfo.BaseType members. 1 million of executions of PCL version takes about 130ms against 110ms with use of delegate for getting BaseType value directly from Type. Not much of a difference, but since this method is used a lot it is worth a shot.
To cache this function we can save it to static field in static constructor.
static TypeExtensions() { var parameterExpression = Expression.Parameter(typeof(Type)); GetBaseTypeFunc = (Func<Type, Type>)Expression.Lambda(Expression.Property( parameterExpression, "BaseType"), parameterExpression).Compile(); }
And it is used exactly the same as BaseType property:
public static bool IfHaveBaseClassOf(this Type type) where TBase : class { var haveBase = false; var typeToCheck = type; var baseType = typeof(TBase); while (typeToCheck != null) { if (GetBaseTypeFunc(typeToCheck) == baseType) { haveBase = true; break; } typeToCheck = GetBaseTypeFunc(typeToCheck); } return haveBase; }
Ok. But this is only one missing property and code that mitigate this limitation is quite complex. Also code like that do not get checked at compile time. But splitting it in to PCL and platform specific implementations would be quite complex since it is static class and we cannot just make it abstract and override single method. Making it instance class would not make things easier too, since we can’t use platform specific implementation directly in PCL. It would force us to write interface in PCL and then use its platform implementation via for example static reference in App class. This would be even more complex than compiling the delegate, and quite few things to do just to use one unavailable property. With solution as above we at least have it contained into one class and one file and we can forget about this inconvenience. It works and with few tests for this method it should be safe.
Splitting rest of classes between PCL and platform implementation will not be that complicated. TypeExtensions, ViewAssemblyAttribute, ViewData can be placed in PCL without changes. ViewFactory have to split into two parts. PCL version will be called BaseViewFactory and platform specific (like Android in sample) will be just ViewFactory.
There are two things that need platform specific implementation:
- Type.GetContstructor(Typep[]) method is not available in PCL
- AppDomain class is not available in PCL
- Assembly.GetExecutingAssembly method is not available in PCL
For first two we have to create abstract methods (and BaseViewFactory need to be abstract by extent).
protected abstract Assembly[] GetViewAssemblies(); protected abstract ConstructorInfo GetDefaultConstructor(Type page)
In platform version of ViewFactory we have:
public class ViewFactory : BaseViewFactory { protected override Assembly[] GetViewAssemblies() { return AppDomain.CurrentDomain.GetAssemblies(); } protected override ConstructorInfo GetDefaultConstructor(Type page) { return page.GetConstructor(Type.EmptyTypes); } }
In summary it is the same code that was previously in single ViewFactory class, but now is called via abstract methods. Unfortunately calling those two method forces as to make Init method non static. And this forces us to make some static reference to ViewFactory in example in App class or use Singleton pattern. For case of sample simplicity static instance will suffice.
public partial class App { public static readonly VF ViewFactory = new VF(); public App() { InitializeComponent(); ViewFactory.Init(); MainPage = MasterDetailControl.CreateMainPage<MasterDetail, MasterDetailViewModel>(); Navigation = MainPage.Navigation; } }
As you can see it is just static global instance. Nothing fancy. IoC would allow us to make it much better.
Another problem with new code is that, since it do not reside in application assembly, we do not have easy access to it. Calling code like below in ViewFactory class (platform part):
Debug.WriteLine(Assembly.GetExecutingAssembly()); Debug.WriteLine(Assembly.GetCallingAssembly()); Debug.WriteLine(Assembly.GetEntryAssembly());
will give us following result:
[0:] ViewFactory.Android, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null [0:] ViewFactory, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null [0:]
First is Android platform version of ViewFactory project, second is PCL ViewFactory assembly, and third is null. Of course this assembly is inside loaded assemblies list and we could find it by App or MainAcitivity types, but we would have to iterate both: assemblies and types in each of them, and we wanted not to do that.
Simple way to solve this issue it to just call Assembly.GetExecutingAssembly() inside application assembly and pass return value to ViewFactory. Let us change Init method once again and pass appropriate value inside App type constructor.
public void Init(Assembly appAssembly) ViewFactory.Init(Assembly.GetExecutingAssembly());
And this is why we do not need to fix lack of Assembly.GetExecutingAssembly method in PCL. We do not need to call it there anyway.
Non standard view model – view connection
It might be case when there is need to have connection between view model and view other than by name convention (or if this is just impossible to rename view or view model). For this case it would be nice to have possibility of defining view type. We can solve this by adding new attribute for view models, that allows us to define desired view type.
[AttributeUsage(AttributeTargets.Class)] public class ViewTypeAttribute : Attribute { public ViewTypeAttribute(Type viewType) { ViewType = viewType; } public Type ViewType { get; } }
This attribute it is used like this:
[ViewType(typeof(NoViewModelView))] public class NoViewViewModel : BaseViewModel
Very easy and convenient. But applying it to view model forces us to change ScanAssembly method a little, so it reads view models first and then tries to found appropriate views for them.
private void ScanAssembly(Assembly appAssebly, bool @override = false) { var pages = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<UIPage>()).ToArray(); var viewModels = appAssebly.DefinedTypes.Where(t => t.IfHaveBaseClassOf<BaseViewModel>()); foreach (var viewModel in viewModels) { var viewTypeAttribute = viewModel.GetCustomAttribute<ViewTypeAttribute>(); var page = viewTypeAttribute != null ? viewTypeAttribute.ViewType : pages.FirstOrDefault(p => p.Name == viewModel.Name.Replace(ViewModelPrefix, ""))?.AsType(); if (page != null) { var asType = viewModel.AsType(); if (!Views.ContainsKey(asType)) { Views.Add(asType, GetViewData(asType, page)); } } } //no view model in assembly and if app assembly -> override if (@override) { foreach (var page in pages) { var viewToOverride = Views.FirstOrDefault(kv => kv.Key.Name == page.Name + ViewModelPrefix); if (viewToOverride.Key != null) { Views[viewToOverride.Key] = GetViewData(viewToOverride.Value.ViewModelType, page.AsType()); } } } }
Its pretty much the same code, but instead of iterating pages we are iterating view models.
New logic involves looking for above attribute. Code is mostly self-explanatory. First its search for attribute on view model and if it was applied just uses type defined there. If not it searches inside collection of pages in assembly. Since Assembly.DefinedTypes is collection of TypeInfo objects null propagation and AsType method is used.
var viewTypeAttribute = viewModel.GetCustomAttribute<ViewTypeAttribute>(); var page = viewTypeAttribute != null ? viewTypeAttribute.ViewType : pages.FirstOrDefault(p => p.Name == viewModel.Name.Replace(ViewModelPrefix, ""))?.AsType();
There are some cases when several views are connected to the same view model. It might be some kind of wizard or other process that require several steps and each step is represent by different view. To minimize effect of state on application (many bugs in my code, including ones that are really hard to debug where connected to state of objects, corrupted or otherwise incorrect, so I can agree to some extent when someone is using phrase: ‘state is evil’) it is good to have the same view model for all of those views. How to accomplish that with our view factory?
The easiest way to do it is to have overload of CreateView method that takes view model and view types. Yes, it defeats purpose of view factory to get rid of views in view models navigation code, but is makes code much more readable and intuitive. Otherwise we would have to create some kind of mechanism of selecting of view from view model state that is transparent to view factory. How? It would be possible with special view model type, with method or property, that would return desired type of view. But it would be too rigid to force base class of view models just for that. More flexible would be to possibility of defining method or property visible to view factory that would return view type. In both we would have to deal with view types anyway. I was considering for some time creating attributes for views that would allow us to define method or property name and desired value returned that would indicate, that it is time for this particular view. I think that it would be viable since view model probably would have some internal state value, that would point to some particular step (number of step for example). However it would force us to place view model method name in attribute which would be hard to maintain and not very transparent.
So the most reasonable way is to just allow to define view type for view model in CreateView method.
public UIPage CreateView<TViewModel, TView>() where TViewModel : BaseViewModel where TView : UIPage { var type = typeof(TView); if (!_unconnectedViews.ContainsKey(type)) { _unconnectedViews[type] = GetViewCreator(type); } return CreateView(typeof(TViewModel), _unconnectedViews[type]); } public UIPage CreateView<TViewModel, TView>(TViewModel viewModel) where TViewModel : BaseViewModel where TView : UIPage { var type = typeof(TView); if (!_unconnectedViews.ContainsKey(type)) { _unconnectedViews[type] = GetViewCreator(type); } return CreateView(viewModel, _unconnectedViews[type]); }
Those two new overloads are used, first navigate to wizard view model and view type (overload with two types) and then navigate to all subsequent steps (overload with view model instance and view type).
Private static CreateView methods needs to be changed too.
private static UIPage CreateView(Type viewModelType, Func<UIPage> creator) { var viewModel = Activator.CreateInstance(viewModelType); return CreateView(viewModel, creator); } private static UIPage CreateView(object viewModel, Func<UIPage> creator) { var page = creator(); page.BindingContext = viewModel; return page; }
Now we can test both, new features in action. First custom connection between view and view models. For this we should create new view and view model.
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ViewFactorySample.View.NoViewModelView"> <Label Text="No view model view" VerticalOptions="Center" HorizontalOptions="Center" FontSize="40"/> </customMasterDetailControl:DetailPage>
[ViewType(typeof(NoViewModelView))] public class NoViewViewModel : BaseViewModel { }
Nothing very different from previous views and view models. We need just to add new button in main view:
private ICommand _toCustomConnection; public ICommand ToCustomConnection { get { return _toCustomConnection ?? (_toCustomConnection = new Command(OnToCustomConnection)); } } private void OnToCustomConnection() { Detail = _viewFactory.CreateView<NoViewViewModel>(); }
<Button Text="To custom connection page" Command="{Binding ToCustomConnection}" />
After running changed application and clicking on new button we will see page like below.
Custom connection created by attribute works.
Let’s test navigating to views with the same view model.
For this we have to create new view model and (at least) two views. Second view is almost the same as previous ones (with different label text). First have one button more, to allow us to test navigation to second step.
<?xml version="1.0" encoding="utf-8" ?> <customMasterDetailControl:DetailPage x_Class="ViewFactorySample.View.Step1View"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="50"/> </Grid.RowDefinitions> <Label Grid.Row="0" Text="Step 1" VerticalOptions="Center" HorizontalOptions="Center" FontSize="40" /> <Button Grid.Row="1" Text="To next step" Command="{Binding ToNextStep}"></Button> </Grid> </customMasterDetailControl:DetailPage>
ToNextStep command is handled in view model.
using System.Windows.Input; using CustomMasterDetailControl; using ViewFactorySample.View; using Xamarin.Forms; namespace ViewFactorySample.ViewModels { public class WizardViewModel : BaseViewModel { private ICommand _toNextStep; public ICommand ToNextStep { get { return _toNextStep ?? (_toNextStep = new Command(OnToNextStep)); } } private void OnToNextStep() { var mainPage = ((NavigationPage)App.Current.MainPage).CurrentPage; var masterPageViewModel = (MasterDetailViewModel)mainPage.BindingContext; masterPageViewModel.Detail = masterPageViewModel.ViewFactory.CreateView<WizardViewModel, Step2View>(this); } } }
As you can see command is pretty complicated, but almost all of the code is to retrieve reference to MasterPageViewModel to set new page as detail. This is major pain and I will cover in next article how to achieve easy and uniform navigation to ordinary, whole screen pages and detail ones.
Last thing to make this test works is new button on main page (exactly the same as before, with different text and command). It is handled a bit differently, to show how to create page with new CreateView overload.
private void OnToWizard() { Detail = _viewFactory.CreateView<WizardViewModel, Step1View>(); }
After running application and using new menu button it will navigate to first step view. Button at bottom will then navigate to second step.
That is all. We have working ViewFactory class that allows us to create page – view model pairs in easily and configurable way. But as you can see from MasterDetailViewModel and WizardViewModel there are still some problems with navigation to whole screen pages and detail pages. Since code is different we need to know in two places if page is detail page or not. It is redundant since we already set that by choosing base class (UIPage or DetailPage). There is need for new mechanism that takes care of that – either set page to Detail property of MasterPage or push it to navigation. Also ViewFactory always creates view models with Activator.CreateInstance which require view model to have default, parameterless constructor. We do not want that, but to fix that we need IoC container to resolve view models with dependencies.
I will cover both topics in next articles.
Sample application you can find here ViewFactory.zip (83.71 kb)
Code for this article is also available on github as previous ones from Xamarin series.