Last active
February 18, 2019 06:25
-
-
Save taimila/4802b9df80f697889c78e66d6b384b53 to your computer and use it in GitHub Desktop.
This is a experimental way of implementing MVVM in Xamarin Forms.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
andObservableObject
are from MVVM Helpers.