TinyMVVM is a super light MVVM Framework designed specifically for Xamarin.Forms. It's designed to be Easy, Simple and Flexible.
- It's super light and super simple
- It's specifically designed for Xamarin.Forms
- Designed to be easy to learn and develop (great when you are not ready for RxUI)
- Uses a Convention over Configuration
- ViewModel to ViewModel Navigation
- Automatic wiring of BindingContext
- Automatic wiring of Page events (eg. appearing)
- Basic methods (with values) on ViewModel (init, reverseinit, oncreated, ondisposed)
- Built-in IoC Container
- ViewModel Constructor Injection
- Basic methods available in Model, like Alert
- Built-in Navigation types for SimpleNavigation, Tabbed and MasterDetail
This Framework, while simple, is also powerful and uses a Convention over Configuration style.
- A Page must have a corresponding ViewModel, with naming important so a QuoteViewModel must have a QuotePage. The BindingContext on the page will be automatically set with the Model
- A ViewModel can have a Init method that takes a object
- A ViewModel can have a ReverseInit method that also take a object and is called when a model is poped with a object
- A ViewModel can get multi object from NavigationParameters
- ViewModel can have dependancies automatically injected into the Constructor
- A ViewModel's name must end with ViewModel and have namespace begin with YourNamepace.ViewModels
- A Page's name name must end with Page and have namespace begin with YourNamepace.Views
Create class use:
public class YourClass : TinyViewModel
{
}
Create Navigaton use:
var page = ViewModelResolver.ResolveViewModel<YourClass>();
MainPage = new NavigationContainer(page);
The Primary form of Navigation in TinyMVVM is ViewModel to ViewModel, this essentially means our views have no idea of Navigation.
So to Navigate between ViewModel use:
await CoreMethods.PushViewModel<QuoteViewModel>(); // Pushes navigation stack
await CoreMethods.PushViewModel<QuoteViewModel>(null, true); // Pushes a Modal
The engine for Navigation in TinyMVVM is done via a simple interface, with methods for Push and Pop. Essentially these methods can control the Navigation of the application in any way they like.
public interface INavigationService
{
Task PushViewModel(Page page, TinyViewModel model, bool modal = false);
Task PushViewModel(bool modal = false);
}
Within the PushPage and PopPage you can do any type of navigation that you like, this can be anything from a simple navigation to a advanced nested navigation.
You can pass object via data in PushViewModel methods.
Task PushViewModel<T>(object data, bool modal = false, bool animate = true) where T : TinyViewModel;
Task PushViewModel(Type viewModelType, object data = null, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, Type pageType, object data = null, bool modal = false, bool animate = true);
Task PushViewModel<T, TPage>(object data, bool modal = false, bool animate = true) where T : TinyViewModel where TPage : Page;
Then you can get object via Init method.
public override void Init(object data)
{
base.Init(data);
var value = (ClassType)data;
}
You can pass many object via Parameters
Task PushViewModel<T>(NavigationParameters parameters, bool modal = false, bool animate = true) where T : TinyViewModel;
Task PushViewModel(Type viewModelType, NavigationParameters parameters, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, Type pageType, NavigationParameters parameters, bool modal = false, bool animate = true);
Task PushViewModel<T, TPage>(NavigationParameters parameters, bool modal = false, bool animate = true) where T : TinyViewModel where TPage : Page;
Then you can get Parameters's Value from Overide Method (Init, OnPageCreated, OnPushed), Parameters's Value is still Null at Constructor.
public override void OnPageCreated()
{
base.OnPageCreated();
var value = Parameters.GetValue<YourClass>("ClassName");
}
The Framework contains some built in Navigation containers for the different types of Navigation.
var page = ViewModelResolver.ResolveViewModel<MainMenuViewModel>();
var basicNavContainer = new NavigationContainer(page);
MainPage = basicNavContainer;
var masterDetailNav = new MasterDetailNavigationContainer();
masterDetailNav.Init("Menu");
masterDetailNav.AddPage<ContactListViewModel>("Contacts", null);
masterDetailNav.AddPage<QuoteListViewModel>("Pages", null);
MainPage = masterDetailNav;
var tabbedNavigation = new TabbedNavigationContainer();
tabbedNavigation.AddTab<ContactListViewModel>("Contacts", null);
tabbedNavigation.AddTab<QuoteListViewModel>("Pages", null);
MainPage = tabbedNavigation;
It's possible to setup any type of Navigation by implementing INavigationService.There's a sample of this in Sample Application named CustomImplementedNav.cs.
- Basic Navigation Sample
- Tabbed Navigation Sample
- MasterDetail Navigation Sample
- Tabbed Navigation with MasterDetail Popover Sample (This is called the CustomImplementedNav in the Sample App)
So that you don't need to include your own IoC container, TinyMVVM comes with a IoC Container built-in. It's using TinyIoC underneith.
TinyIOC.Container.Register<IDatabaseService, DatabaseService>();
TinyIOC.Container.Resolve<IDatabaseService>();
*This is also what drives constructor injection.
We now support a fluent API for setting the object lifetime of object inside the IoC Container.
// By default we register concrete types as
// multi-instance, and interfaces as singletons
TinyIOC.Container.Register<MyConcreteType>(); // Multi-instance
TinyIOC.Container.Register<IMyInterface, MyConcreteType>(); // Singleton
// Fluent API allows us to change that behaviour
TinyIOC.Container.Register<MyConcreteType>().AsSingleton(); // Singleton
TinyIOC.Container.Register<IMyInterface, MyConcreteType>().AsMultiInstance(); // Multi-instance
As you can see below the ITinyIOC interface methods return the IRegisterOptions interface.
public interface ITinyIOC
{
object Resolve(Type resolveType);
IRegisterOptions Register<RegisterType>(RegisterType instance) where RegisterType : class;
IRegisterOptions Register<RegisterType>(RegisterType instance, string name) where RegisterType : class;
ResolveType Resolve<ResolveType>() where ResolveType : class;
ResolveType Resolve<ResolveType>(string name) where ResolveType : class;
IRegisterOptions Register<RegisterType, RegisterImplementation> ()
where RegisterType : class
where RegisterImplementation : class, RegisterType;
}
The interface that's returned from the register methods is IRegisterOptions.
public interface IRegisterOptions
{
IRegisterOptions AsSingleton();
IRegisterOptions AsMultiInstance();
IRegisterOptions WithWeakReference();
IRegisterOptions WithStrongReference();
IRegisterOptions UsingConstructor<RegisterType>(Expression<Func<RegisterType>> constructor);
}
When ViewModels are pushed services that are in the IOC container can be pushed into the Constructor.
TinyIOC.Container.Register<IDatabaseService, DatabaseService>();
/// <summary>
/// The previous view model, that's automatically filled, on push
/// </summary>
public TinyViewModel PreviousViewModel { get; set; }
/// <summary>
/// A reference to the current page, that's automatically filled, on push
/// </summary>
public Page CurrentPage { get; set; }
/// <summary>
/// Core methods are basic built in methods for the App including Pushing, Pop and Alert
/// </summary>
public IViewModelCoreMethods CoreMethods { get; set; }
/// <summary>
/// Parameters passed from previous ViewModel
/// </summary>
public NavigationParameters Parameters { get; set; } = new NavigationParameters();
/// <summary>
/// This method is called when the ViewModel is loaded, the initData is the data that's sent from ViewModel before
/// </summary>
/// <param name="initData">Data that's sent to this ViewModel from the pusher</param>
public virtual void Init(object initData)
{
}
/// <summary>
/// This method is called when a page is Pop'd, it also allows for data to be returned.
/// </summary>
/// <param name="returndData">This data that's returned from </param>
public virtual void ReverseInit(object returndData)
{
}
/// <summary>
/// This method is called when after page is created and bindingcontext is assigned to page.
/// </summary>
public virtual void OnPageCreated()
{
}
/// <summary>
/// This method is called when a page is Push'd or is set as page root in navigation stack.
/// </summary>
public virtual void OnPushed()
{
}
/// <summary>
/// This method is called when a page is Pop'd.
/// </summary>
public virtual void OnPopped()
{
}
/// <summary>
/// This method is called at destructor.
/// </summary>
public virtual void OnDisposed()
{
}
/// <summary>
/// This methods is called when the View is appearing
/// </summary>
protected virtual void ViewIsAppearing (object sender, EventArgs e)
{
}
/// <summary>
/// This method is called when the view is disappearing.
/// </summary>
protected virtual void ViewIsDisappearing (object sender, EventArgs e)
{
}
Each ViewModel has a property called 'CoreMethods' which is automatically filled when a ViewModel is pushed, it's the basic functions that most apps need like Alerts, Pushing, Poping etc.
public interface IViewModelCoreMethods
{
Task DisplayAlert(string title, string message, string cancel);
Task<bool> DisplayAlert(string title, string message, string accept, string cancel);
Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons);
Task PushPage<T>(bool modal = false, bool animate = true) where T : Page;
Task PushPage(Type pageType, bool modal = false, bool animate = true);
Task PushPage(Page page, bool modal = false, bool animate = true);
Task PushViewModel<T>(bool modal = false, bool animate = true) where T : TinyViewModel;
Task PushViewModel<T>(object data, bool modal = false, bool animate = true) where T : TinyViewModel;
Task PushViewModel<T>(NavigationParameters parameters, bool modal = false, bool animate = true) where T : TinyViewModel;
Task PushViewModel(Type viewModelType, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, object data = null, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, NavigationParameters parameters, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, Type pageType, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, Type pageType, object data = null, bool modal = false, bool animate = true);
Task PushViewModel(Type viewModelType, Type pageType, NavigationParameters parameters, bool modal = false, bool animate = true);
Task PushViewModel<T, TPage>(bool modal = false, bool animate = true) where T : TinyViewModel where TPage : Page;
Task PushViewModel<T, TPage>(object data, bool modal = false, bool animate = true) where T : TinyViewModel where TPage : Page;
Task PushViewModel<T, TPage>(NavigationParameters parameters, bool modal = false, bool animate = true) where T : TinyViewModel where TPage : Page;
Task PopViewModel(bool modal = false, bool animate = true);
}
It’s always been possible to do any type of navigation in TinyMVVM, with custom or advanced scenarios were done by implementing a custom navigation service. Even with this ability people found it a little hard to do advanced navigation scenarios in TinyMVVM. After I reviewed all the support questions that came in for TinyMVVM I found that the basic issue people had was they wanted to be able to use our built in navigation containers multiple times, two primary examples are 1) within a master detail having a navigation stack in a master and another in the detail 2) The ability to push modally with a new navigation container. In order to support both these scenarios I concluded that the TinyMVVM required the ability to have named NavigationServices so that we could support multiple NavigationService’s.
Below we’re running two navigation stacks, in a single MasterDetail.
var masterDetailsMultiple = new MasterDetailPage(); //generic master detail page
//we setup the first navigation container with ContactList
var contactListPage = ViewModelResolver.ResolveViewModel<ContactListViewModel>();
contactListPage.Title = "Contact List";
//we setup the first navigation container with name MasterPageArea
var masterPageArea = new NavigationContainer(contactListPage, "MasterPageArea");
masterPageArea.Title = "Menu";
masterDetailsMultiple.Master = masterPageArea; //set the first navigation container to the Master
//we setup the second navigation container with the QuoteList
var quoteListPage = ViewModelResolver.ResolveViewModel<QuoteListViewModel>();
quoteListPage.Title = "Quote List";
//we setup the second navigation container with name DetailPageArea
var detailPageArea = new FreshNavigationContainer(quoteListPage, "DetailPageArea");
masterDetailsMultiple.Detail = detailPageArea; //set the second navigation container to the Detail
MainPage = masterDetailsMultiple;
//push a basic page Modally
var page = ViewModelResolver.ResolveViewModel<MainMenuViewModel>();
var basicNavContainer = new NavigationContainer(page, "secondNavPage");
await CoreMethods.PushNewNavigationServiceModal(basicNavContainer, new TinyViewModel[] { page.GetModel() });
//push a tabbed page Modally
var tabbedNavigation = new TabbedNavigationContainer ("secondNavPage");
tabbedNavigation.AddTab<ContactListViewModel>("Contacts", "contacts.png", null);
tabbedNavigation.AddTab<QuoteListViewModel> ("Quotes", "document.png", null);
await CoreMethods.PushNewNavigationServiceModal(tabbedNavigation);
//push a master detail page Modally
var masterDetailNav = new MasterDetailNavigationContainer("secondNavPage");
masterDetailNav.Init("Menu", "Menu.png");
masterDetailNav.AddPage<ContactListViewModel>("Contacts", null);
masterDetailNav.AddPage<QuoteListViewModel>("Quotes", null);
await CoreMethods.PushNewNavigationServiceModal(masterDetailNav);
There's some cases in Xamarin.Forms you might want to run multiple navigation stacks. A good example of this is when you have a navigation stack for the authentication and a stack for the primary area of your application.
To begin with we can setup some names for our navigation containers.
public class NavigationContainerNames
{
public const string AuthenticationContainer = "AuthenticationContainer";
public const string MainContainer = "MainContainer";
}
Then we can create our two navigation containers and assign to the MainPage.
var loginPage = ViewModelResolver.ResolveViewModel<LoginViewModel>();
var loginContainer = new NavigationContainer(loginPage, NavigationContainerNames.AuthenticationContainer);
var myPitchListViewContainer = new TabbedNavigationContainer(NavigationContainerNames.MainContainer);
MainPage = loginContainer;
The Navigation Container will use the name passed as argument to register in this method
public TabbedNavigationContainer(string navigationServiceName)
{
NavigationServiceName = navigationServiceName;
RegisterNavigation();
}
protected void RegisterNavigation()
{
TinyIOC.Container.Register<INavigationService>(this, NavigationServiceName);
}
Once we've set this up we can now switch out our navigation containers.
CoreMethods.SwitchOutRootNavigation(NavigationContainerNames.MainContainer);
That name will be resolved in this method to find the correct Navigation Container
public void SwitchOutRootNavigation(string navigationServiceName)
{
INavigationService rootNavigation = TinyIOC.Container.Resolve<INavigationService>(navigationServiceName);
}
The second major request for TinyMVVM was to allow custom IoC containers. In the case that your application already has a container that you want to leverage.
Using a custom IoC container is very simple in that you only need to implement a single interface.
public interface ITinyIOC
{
object Resolve(Type resolveType);
void Register<RegisterType>(RegisterType instance) where RegisterType : class;
void Register<RegisterType>(RegisterType instance, string name) where RegisterType : class;
ResolveType Resolve<ResolveType>() where ResolveType : class;
ResolveType Resolve<ResolveType>(string name) where ResolveType : class;
void Register<RegisterType, RegisterImplementation> ()
where RegisterType : class
where RegisterImplementation : class, RegisterType;
}
And then set the IoC container in the System.
TinyIOC.OverrideContainer(myContainer);
WhenAny is an extension method on INotifyPropertyChanged, it's a shorthand way to subscribe to a property changed event.
In the example below, we use any to link up
[PropertyChanged.AddINotifyPropertyChangedInterface]
public class ContactViewModel : TinyViewModel
{
public ContactViewModel()
{
this.WhenAny(HandleContactChanged, o => o.Contact);
}
void HandleContactChanged(string propertyName)
{
//handle the property changed, nice
}
public Contact Contact { get; set; }
}