Skip to content

Instantly share code, notes, and snippets.

@taimila
Last active February 18, 2019 06:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taimila/4802b9df80f697889c78e66d6b384b53 to your computer and use it in GitHub Desktop.
Save taimila/4802b9df80f697889c78e66d6b384b53 to your computer and use it in GitHub Desktop.
This is a experimental way of implementing MVVM in Xamarin Forms.
using System;
using Xamarin.Forms;
using System.Linq;
using System.Threading.Tasks;
namespace ExampleApp
{
/// <summary>
/// Base content page that provides navigation mechanism that
/// allows view models to navigate from one view model to another
/// without depending on views. This implementation does not use
/// reflection to create pages, but factory class instead. This
/// is a choice to improve runtime performance of the app.
/// </summary>
public class BaseContentPage<T> : ContentPage where T: BaseViewModel
{
protected T Model { get; private set; }
public BaseContentPage()
{
Init(GetDesignTimeData());
}
/// <summary>
/// Override this method in inherited class and return
/// a model with design time data.
/// </summary>
/// <returns>The design time data.</returns>
protected virtual T GetDesignTimeData()
{
return Activator.CreateInstance<T>();
}
public BaseContentPage(T model)
{
Init(model);
}
void Init(T model)
{
Model = model;
BindingContext = model;
model.OnNavigation += OnNavigation;
model.OnModalNavigation += OnModalNavigation;
model.OnBackNavigation += OnBackNavigation;
}
void OnNavigation(object sender, NavigationEventArgs e)
{
Navigation.PushAsync(PageFactory.Create(e.Model));
}
void OnModalNavigation(object sender, NavigationEventArgs e)
{
Navigation.PushModalAsync(PageFactory.Create(e.Model));
}
void OnBackNavigation(object sender, EventArgs e)
{
if (Navigation.ModalStack.Any() && Navigation.ModalStack.First() == this)
Navigation.PopModalAsync();
else
Navigation.PopAsync();
}
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
if (this.width == width && this.heigth == height)
return;
this.width = width;
this.heigth = height;
OnPageSizeChanged(new Size(width, height));
}
/// <summary>
/// Override this instead of OnSizeAllocated() and you won't get
/// multiple calls for same size.
/// </summary>
/// <param name="size">Size.</param>
protected virtual void OnPageSizeChanged(Size size) { }
}
}
using System;
using System.Linq;
using Xamarin.Forms;
namespace ExampleApp
{
/// <summary>
/// Base content view that provides navigation mechanism that
/// allows view models to navigate from one view model to another
/// without depending on views. This implementation does not use
/// reflection to create pages, but factory class instead. This
/// is a choice to improve runtime performance of the app.
/// </summary>
public class BaseContentView<T> : ContentView where T : BaseViewModel
{
protected T Model { get; private set; }
public BaseContentView()
{
Init(GetDesignTimeData());
}
/// <summary>
/// Override this method in inherited class and return
/// a model with design time data.
/// </summary>
/// <returns>The design time data.</returns>
protected virtual T GetDesignTimeData()
{
return Activator.CreateInstance<T>();
}
public BaseContentView(T model)
{
Init(model);
}
void Init(T model)
{
Model = model;
BindingContext = model;
model.OnNavigation += OnNavigation;
model.OnModalNavigation += OnModalNavigation;
model.OnBackNavigation += OnBackNavigation;
}
void OnNavigation(object sender, NavigationEventArgs e)
{
Navigation.PushAsync(PageFactory.Create(e.Model));
}
void OnModalNavigation(object sender, NavigationEventArgs e)
{
Navigation.PushModalAsync(PageFactory.Create(e.Model));
}
void OnBackNavigation(object sender, EventArgs e)
{
if (Navigation.ModalStack.Any()) //TODO: Nested modal pages might fail here
Navigation.PopModalAsync();
else
Navigation.PopAsync();
}
}
}
using System;
namespace ExampleApp
{
/// <summary>
/// Base view model.
/// </summary>
public class BaseViewModel : ObservableObject
{
#region Navigation events
public EventHandler<NavigationEventArgs> OnNavigation;
public void NavigateTo(BaseViewModel model) => OnNavigation.Invoke(this, new NavigationEventArgs(model));
public EventHandler<NavigationEventArgs> OnModalNavigation;
public void NavigateModalTo(BaseViewModel model) => OnModalNavigation.Invoke(this, new NavigationEventArgs(model));
public EventHandler OnBackNavigation;
public void NavigateBack() => OnBackNavigation.Invoke(this, new EventArgs());
#endregion
string title = string.Empty;
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>The title.</value>
public string Title
{
get => title;
set => SetProperty(ref title, value);
}
string subtitle = string.Empty;
/// <summary>
/// Gets or sets the subtitle.
/// </summary>
/// <value>The subtitle.</value>
public string Subtitle
{
get => subtitle;
set => SetProperty(ref subtitle, value);
}
string icon = string.Empty;
/// <summary>
/// Gets or sets the icon.
/// </summary>
/// <value>The icon.</value>
public string Icon
{
get => icon;
set => SetProperty(ref icon, value);
}
bool isBusy;
/// <summary>
/// Gets or sets a value indicating whether this instance is busy.
/// </summary>
/// <value><c>true</c> if this instance is busy; otherwise, <c>false</c>.</value>
public bool IsBusy
{
get => isBusy;
set
{
if (SetProperty(ref isBusy, value))
IsNotBusy = !isBusy;
}
}
bool isNotBusy = true;
/// <summary>
/// Gets or sets a value indicating whether this instance is not busy.
/// </summary>
/// <value><c>true</c> if this instance is not busy; otherwise, <c>false</c>.</value>
public bool IsNotBusy
{
get => isNotBusy;
set
{
if (SetProperty(ref isNotBusy, value))
IsBusy = !isNotBusy;
}
}
bool canLoadMore = true;
/// <summary>
/// Gets or sets a value indicating whether this instance can load more.
/// </summary>
/// <value><c>true</c> if this instance can load more; otherwise, <c>false</c>.</value>
public bool CanLoadMore
{
get => canLoadMore;
set => SetProperty(ref canLoadMore, value);
}
string header = string.Empty;
/// <summary>
/// Gets or sets the header.
/// </summary>
/// <value>The header.</value>
public string Header
{
get => header;
set => SetProperty(ref header, value);
}
string footer = string.Empty;
/// <summary>
/// Gets or sets the footer.
/// </summary>
/// <value>The footer.</value>
public string Footer
{
get => footer;
set => SetProperty(ref footer, value);
}
}
}
[Serializable]
public sealed class NavigationEventArgs : EventArgs
{
public NavigationEventArgs(BaseViewModel model)
{
Model = model;
}
public BaseViewModel Model { get; }
}
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
namespace ExampleApp
{
/// <summary>
/// Observable object with INotifyPropertyChanged implemented
/// </summary>
public class ObservableObject : INotifyPropertyChanged
{
/// <summary>
/// Sets the property.
/// </summary>
/// <returns><c>true</c>, if property was set, <c>false</c> otherwise.</returns>
/// <param name="backingStore">Backing store.</param>
/// <param name="value">Value.</param>
/// <param name="validateValue">Validates value.</param>
/// <param name="propertyName">Property name.</param>
/// <param name="onChanged">On changed.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
protected virtual bool SetProperty<T>(
ref T backingStore, T value,
[CallerMemberName]string propertyName = "",
Action onChanged = null,
Func<T, T, bool> validateValue = null)
{
//if value didn't change
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
//if value changed but didn't validate
if (validateValue != null && !validateValue(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Occurs when property changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event.
/// </summary>
/// <param name="propertyName">Property name.</param>
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
using System;
using Xamarin.Forms;
namespace ExampleApp
{
public static class PageFactory
{
public static Page Create(BaseViewModel model)
{
// TODO: Do View model to View mapping here by checking the view model type
// and creating the corresponding page. I recommend NOT to use reflection
// because it does have negative effect to performance and it adds complexity
// of code. Instead write one simple if every time new page/viewmodel is added.
if (model is ExampleViewModel m)
return new ExamplePage(m);
throw new ArgumentException("No view defined for model " + model.GetType().Name);
}
}
}
@taimila
Copy link
Author

taimila commented Feb 12, 2019

There are multiple existing frameworks attempting to do this, but I find them to be too heavy on code size and performance hit they come with. My goal is to have light weight easy to understand solution, that enabled separation of concerns without reflection. BaseViewModel and ObservableObject are from MVVM Helpers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment