The MVVM Framework Anti-Pattern - Marcus Technical Services
Let’s face it: programmers like frameworks. A framework is a set of pre-packaged code that supports a particular functionality. The more fundamental the functionality, the more vital the framework seems to be. When the entire coding community adopts the framework, that seals the deal. This is what has propelled MVVM Frameworks to the forefront of Xamarin.Forms development. No one has bothered analyzing these frameworks for their alignment with C# SOLID design philosophy.
“The opposite of courage … is not cowardice, it is conformity.”
― Rollo May
Let’s keep things SOLID
SOLID principles ask us to build programs that couple loosely and are open to change as long as the interacting elements obey their interface contracts. This is the first thing that goes out the window with MVVM frameworks.
Here is an example of a view that displays data based on an interface contract:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MvvmAntipattern.StateMachine.Views.Pages.AnimalPage" > <StackLayout VerticalOptions="Start" HorizontalOptions="FillAndExpand" > <Label Text="I Am A:" /> <Label Text="{Binding WhatAmI,Mode=OneWay}" /> <Label Text="I Like To Eat:" Margin="0,10,0,0" /> <Label Text="{Binding LikeToEat,Mode=OneWay}" /> <Label Text="I Am Big" Margin="0,10,0,0" /> <Switch IsToggled="{Binding IAmBig,Mode=TwoWay}" /> <Label Text="I Look Like This:" Margin="0,10,0,0" /> <Image Source="{Binding MyImageSource,Mode=OneWay}" WidthRequest="150" HeightRequest="150" /> <Button Text="Move" Command="{Binding MoveCommand,Mode=OneWay}" Margin="0,10,0,0" /> <Button Text="MakeNoise" Command="{Binding MakeNoiseCommand,Mode=OneWay}" Margin="0,10,0,0" /> </StackLayout> </ContentPage>
The BindingContext can be set to any class that implements this interface:
public interface IAnimal : IMakeBigDecisions { string WhatAmI { get; } string LikeToEat { get; } string MyImageSource { get; } Command MakeNoiseCommand { get; } Command MoveCommand { get; } }
Note the supporting interface:
public interface IMakeBigDecisions { bool IAmBig { get; set; } }
More importantly, the BindingContext is both changeable and unpredictable. That is what we mean by polymorphic, or “open”. Let’s say that the app receives a state change event. That event requires the BindingContext to change to ICat. Or IBird. Or IDog.
public interface ICat : IAnimal { } public interface IBird: IAnimal { } public interface IDog: IAnimal { }
Here are the physical files created by an MVVM framework in this scenario:
AnimalPage.cs
orAnimalView.cs
– the view that interacts with the user.AnimalViewModel.cs
— ImplementsIAnimal.cs
, but does not know aboutICat
,IBird
, oriDog
.AnimalModel.cs
– provides the backing data for theAnimalViewModel
. That is also limited, since it can only provide an animal to an animal view model which then turns it over to the animal view.
There is no way for the MVVM Framework version of the app to do anything except to generalize all animals into a single animal.
View Model to View Model Navigation is a Fantasy
A legend evolved in the middle ages about a Holy Grail. Knights from throughout the world spent years pursuing it. But they never found it. That should give us all a pause when we hear about MVVM frameworks and their holy grail: view model to view model navigation.
Most apps follow this tired-and-true (pun intended) MVVM design pattern:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MvvmAntipattern.TiredAndTrue.TiredAndTrueMainPage"> <ContentPage.Content> <Button Text="Click Me!" Command="{Binding ClickMeCommand,Mode=OneWay}" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" /> </ContentPage.Content> </ContentPage>
public class TiredAndTrueMainViewModel { public TiredAndTrueMainViewModel() { ClickMeCommand = newCommand(() => { var nextPage = newTiredAndTrueSecondPage() { BindingContext = newTiredAndTrueSecondViewModel() }; Application.Current.MainPage = nextPage; }); } public ICommand ClickMeCommand { get; set; } }
The same thing applies to master detail scenarios with menus, where the button resides in a _ menu _ rather than on a _ page _.
The problem is that the page (or menu) determines the next view and view model. That approach is _ closely coupled _. The page is not omniscient enough to know exactly what to do in any given scenario.
It is not hard to understand how the notion of view model navigation began. In MVVM, we perceive most events and bindings at the view model. But MVVM also requires us to keep the view model _ separated _ from the view. What else is left? The _ next _ view model.
Every fantasy is the same: we want something we can’t have. We are willing to do anything to get it. That leads to leaps of faith and rash decisions.
Tricks Are Fun; That’s Where the Fun Ends
MVVM frameworks make the leap from the view model to another view model (and the implied view) through a simple _ file naming convention _. This would have been laughable a decade ago, when we abandoned Visual Basic because it was so hacked-together and sloppy. Now this sort of logic makes perfect “sense”.
For the last example, the MVVM framework requires us to create these files:
TiredAndTrueSecondViewModel.cs
TiredAndTrueViewModel.cs
TiredAndTrueSecondPage.xaml
TiredAndTrueSecondViewModel.cs
In order to navigate from the main page to the second page, the only real information used is _ text prefix _ of the file name:
TiredAndTrueMain
TiredAndTrueSecond
The framework _ reflects on the source _ to _ automatically instantiate _ the view model and its target view.
This is _ not _ a form of navigation. _ It is a reflection trick _. The navigation is not essential – or limited to – the view model. That is an illusion. One can open any view model and view from any location in the app, and at any time. So this is not accurately described or implemented. This is actually _ string to view model navigation _.
Activator.CreateInstance Is Not Your Friend
The MVVM framework leverages Activator.CreateInstance
and similar system-level tools to manage its instantiations. These methods are dangerous in a mobile app because they make these instantiated classes “invisible” to the compiler. The compile-time linker ignores any class that is not directly tied to the source through a normal programmatic relationship. The “trick” is too clever for the compiler. The hack that cures the limitation (the PRESERVE attribute) is ungainly and tedious.
The worst problem with MVVM frameworks is not technical at all, but the extraordinary violation of SOLID principles. This approach requires a one-to-one alignment between:
- The view
- The view model
- The model
This is _ ludicrous _. SOLID says that a view may animate any number of view models, which can change at run-time, and that the data for the view models can also be injected on-the-fly. MVVM frameworks require programmers to create “toy” apps with simplistic structures and zero design flexibility.
Introducing the State Machine
To create a valuable navigation system, let’s get rid of what we don’t need: a make-believe view-model-to-view-model reflection hack. Then we can define what navigation should really do for an app, and how to accomplish that in an elegant, organized, and simple way. Finally, we should face the hard truth about navigation: something has to be the target, and it won’t be the view model, since that is only discovered at run-time through business logic.
A navigation system:
- Is a _ system-wide service _ accessible at any time by any entity with the permission to change the current page. (To add complexity and nuance, we can create multiple navigation services, but that is beyond the scope of this analysis.)
- Does _ not _ require a file-naming convention of any kind.
- Possesses _ fine-grained business logic _ to determine how to match views to view models and perhaps even (data) models to view models.
- Determines the next page (view) initially, and then the view model and data thereafter.
- _ Controls instantiation _ to encourage polymorphism. We can pass any sort of parameter to any constructor based on run-time rules. Also, this avoids the linker issues associated with
Activator.CreateInstance
. - _ Maintains its current state _ to help make decisions.
- Avoids navigating from the view if possible. Most navigation changes should be the result of _ events _ or _ commands _. These will often (but not always) occur inside view models. But that does not make this a view model to view model navigation system, as the view model is not the destination of the navigation process. The view model is most likely the origin of that process.
- Encapsulates its logic privately and without unnecessary interaction with other classes.
The State Machine fits this bill.
Interface:
public interface IStateMachineBase : IDisposable { // A way of knowing the current app state, though this should not be commonly referenced. string CurrentAppState { get; } // The normal way of changing states void GoToAppState<T>(string newState, T payload = default(T), bool preventStackPush = false); // Sets the startup state for the app on initial start (or restart). void GoToStartUpState(); // Goes to the default landing page; for convenience only void GoToLandingPage(bool preventStackPush = true); // Access to the forms messenger; also for convenience. IForms MessengerMessenger { get; set; } }
Base Class:
/// <summary> /// A controller to manage which views and view models are shown for a given state /// </summary> public abstract class StateMachineBase : IStateMachineBase { private Page _lastPage; private string _lastAppState; public abstract string AppStartUpState { get; } public void GoToAppState<T>(string newState, T payload = default(T), bool preventStackPush = false) { if (_lastAppState.IsSameAs(newState)) { return; } // Raise an event to notify the nav bar that the back-stack requires modification. // Send in the last app state, *not* the new one. FormsMessengerUtils.Send(new AppStateChangedMessage(_lastAppState, preventStackPush)); // CurrentAppState = newState; _lastAppState = newState; // Not awaiting here because we do not directly change the Application.Current.MainPage. That is done through a message. RespondToAppStateChange(newState, payload, preventStackPush); } // public string CurrentAppState { get; private set; } public abstract IMenuNavigationState[] MenuItems { get; } // Sets the startup state for the app on initial start (or restart). public void GoToStartUpState() { FormsMessengerUtils.Send(new AppStartUpMessage()); GoToAppState<NoPayload>(AppStartUpState, null, true); } public abstract void GoToLandingPage(bool preventStackPush = true); public void Dispose() { ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } protected abstract void RespondToAppStateChange<PayloadT>(string newState, PayloadT payload, bool preventStackPush); protected void CheckAgainstLastPage(Type pageType, Func<Page> pageCreator, Func<IViewModelBase> viewModelCreator, bool preventStackPush) { // If the same page, keep it if (_lastPage != null && _lastPage.GetType() == pageType) { FormsMessengerUtils.Send(new BindingContextChangeRequestMessage { Payload = viewModelCreator(), PreventNavStackPush = preventStackPush }); return; } // ELSE create both the page and view model var page = pageCreator(); page.BindingContext = viewModelCreator(); FormsMessengerUtils.Send(new MainPageChangeRequestMessage { Payload = page, PreventNavStackPush = preventStackPush }); _lastPage = page; } protected virtual void ReleaseUnmanagedResources() { } ~StateMachineBase() { ReleaseUnmanagedResources(); }
This class is abstract and is published in a shared library. For any given app, we derive and override RespondToAppStateChange
to implement the logic:
public class FormsStateMachine : StateMachineBase { public const string NO_APP_STATE = "None"; public const string AUTO_SIGN_IN_APP_STATE = "AttemptAutoSignIn"; public const string ABOUT_APP_STATE = "About"; public const string PREFERENCES_APP_STATE = "Preferences"; public const string NO_ANIMAL_APP_STATE = "No Animal"; public const string CAT_ANIMAL_APP_STATE = "Cat"; public const string BIRD_ANIMAL_APP_STATE = "Bird"; public const string DOG_ANIMAL_APP_STATE = "Dog"; public static readonly string[] APP_STATES = { NO_APP_STATE, AUTO_SIGN_IN_APP_STATE, NO_ANIMAL_APP_STATE, CAT_ANIMAL_APP_STATE, BIRD_ANIMAL_APP_STATE, DOG_ANIMAL_APP_STATE, ABOUT_APP_STATE, PREFERENCES_APP_STATE }; public override string AppStartUpState => AUTO_SIGN_IN_APP_STATE; public override IMenuNavigationState[] MenuItems => new IMenuNavigationState[] { new MenuNavigationState(GetMenuOrderFromAppState(ABOUT_APP_STATE), ABOUT_APP_STATE, "About", ABOUT_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(BIRD_ANIMAL_APP_STATE), BIRD_ANIMAL_APP_STATE, "ANIMALS", BIRD_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(CAT_ANIMAL_APP_STATE), CAT_ANIMAL_APP_STATE, "ANIMALS", CAT_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(DOG_ANIMAL_APP_STATE), DOG_ANIMAL_APP_STATE, "ANIMALS", DOG_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(NO_ANIMAL_APP_STATE), NO_ANIMAL_APP_STATE, "ANIMALS", NO_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(ABOUT_APP_STATE), PREFERENCES_APP_STATE, "Preferences", PREFERENCES_APP_STATE), }; public override void GoToLandingPage(bool preventStackPush = true) { GoToAppState<NoPayload>(NO_ANIMAL_APP_STATE, null, preventStackPush); } protected override void RespondToAppStateChange<PayloadT>(string newState, PayloadT payload, bool preventStackPush) { var titleStr = payload is IMenuNavigationState pageAsNavState ? pageAsNavState.ViewTitle : ""; switch (newState) { case ABOUT_APP_STATE: CheckAgainstLastPage(typeof(DummyPage), () => new DummyPage(), () => new AboutViewModel(this) {PageTitle = titleStr}, preventStackPush); break; case PREFERENCES_APP_STATE: CheckAgainstLastPage(typeof(DummyPage), () => new DummyPage(), () => new PreferencesViewModel(this) { PageTitle = titleStr }, preventStackPush); break; case CAT_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new CatViewModel(this, new CatData()) { PageTitle = titleStr }, preventStackPush); break; case BIRD_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new BirdViewModel(this, new BirdData()) { PageTitle = titleStr }, preventStackPush); break; case DOG_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new DogViewModel(this, new DogData()) { PageTitle = titleStr }, preventStackPush); break; default: //NO_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new NoAnimalViewModel(this, null) { PageTitle = titleStr }, true); break; } } private void AttemptAutoSignIn() { // Assuming true; also, prevent stack push so we don't go back into this state, as it is "finished" GoToAppState<NoPayload>(NO_ANIMAL_APP_STATE, null, true); }
The State Machine creates the next view and view model based on _ business logic _. All parameters are injected here, making it 100% compliant with dependency injection philosophy.
Note that we also attempt to prevent unnecessary changes. If the next page is the same type as the last one, we just pass along the view model rather than destroying the page and recreating it. This *should* work for most cases. If problems arise, feel free to remove this cache.
private void CheckAgainstLastPage( Type pageType, Func<Page> pageCreator, Func<IViewModelBase> viewModelCreator, bool preventStackPush)
PreventStackPush is here in case we navigate to some page that does not support the navigation system. A log-in page is just such an animal. For that page, we would pass false, and the back-stack would ignore this page. The user is prevented from navigation back to log-in. (_ Note : I did not provide a detailed example of this feature_.)
Message-Based Navigation
In the examples above, when we finally decide what to do, we send a Xamarin.Forms
message asking for the change. This is a new way of thinking about navigation and also about events in general. I will cover this topic in a separate article. In brief, the application listens for this change at app.xaml.cs
:
private void MainPageChanged(object sender, MainPageChangeRequestMessage messageArgs) { // Try to avoid changing the page is possible if ( messageArgs?.Payload == null || MainPage == null || ( _lastMainPage != null && _lastMainPage.GetType() == messageArgs.Payload.GetType() ) ) { return; } MainPage = messageArgs.Payload; _lastMainPage = MainPage; } private void BindingContextPageChanged(object sender, BindingContextChangeRequestMessage messageArgs) { if (MainPage != null) { MainPage.BindingContext = messageArgs.Payload; } }
We never try to change Application.Current.MainPage
from anywhere else in this app.
The Navigation / Title Bar
We began this article with a discussion about the negative impact of the MVVM Framework Design Pattern. Now we have drifted a bit off-topic into navigation. I will cover this in-depth in another article. For clarity, here is how I have wired up the new State Machine and navigation system using so the user can interact with it.
Here is a screen-shot of the app’s opening page. Note the blue bare along the top entitled “ANIMALS”. There is no back button on the left, but there is a menu hamburger on the right. This UI element is the NavAndManuBar
(see the source code for more details).
When the user taps the hamburger button the menu opens up:
If the user selects Cat, the page and view model change to display as follows. Notice that we now have a back button.
If the user taps “I Am Big”, the injected data changes, so the visible content responds like this:
The Back Stack
The new “back stack” is a list of app states, not pages. It is a custom control that cleans itself up to prevent duplications. Relying on the app state creates more flexibility in navigation.
Takeaways
The notion of an MVVM “framework” is enticing. But most of the published systems are just short-cuts to relieve programmers from writing any actual code. They also severely violate C# and SOLID design philosophies. Ironically, they are also do not provide much real “IOC”: https://marcusts.com/2018/04/09/the-ioc-container-anti-pattern/.
Hard Proofs
I created a Xamarin.Forms
mobile app to demonstrate the State Machine. The source is available on GitHub at https://github.com/marcusts/xamarin-forms-annoyances. The solution is called MVVMAntipattern.sln
.
All of the code snippets here are also included in the sample solution, though not used in the demo.
The code is published as open source and without encumbrance.