Click here to Skip to main content
15,881,424 members
Articles / All Topics
Technical Blog

The MVVM Framework Anti-Pattern

Rate me:
Please Sign up or sign in to vote.
3.73/5 (8 votes)
14 Apr 2018CPOL9 min read 8.9K   4   11
... 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 post The MVVM Framework Anti-Pattern appeared first on Marcus Technical Services..
 

The MVVM Framework Anti-Pattern

When Did A File-Naming Trick Become A Design Pattern?

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 or AnimalView.cs – the view that interacts with the user.

AnimalViewModel.cs — Implements IAnimal.cs, but does not know about ICat, IBird, or iDog

AnimalModel.cs – provides the backing data for the AnimalViewModel. 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:

TiredAndTrueMainPage.xaml
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 <strong>Activator.CreateInstance</strong>.
  • 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;

	protected StateMachineBase()
	{
		using (var scope = AppContainer.GlobalVariableContainer.BeginLifetimeScope())
		{
			Messenger = scope.Resolve<IFormsMessenger>();
		}
	}

	// Access to the forms messenger; also for convenience.
	public IForms MessengerMessenger { get; set; }

	public void GoToAppState<T>(string newState, T payload = default(T), bool preventStackPush = false)
	{
		CurrentAppState = newState;
		// Not awaiting here because we do not directly change the Application.Current.MainPage.  That is done through a message.
		RespondToAppStateChange(newState, payload, preventStackPush);
	}

	protected abstract void RespondToAppStateChange(string newState, object payload, bool preventStackPush);

	public string CurrentAppState { get; privateset; }

	// Sets the startup state for the app on initial start (or restart).
	public void GoToStartUpState()
	{
		Messenger.Send(newAppStartUpMessage());
		GoToAppState<NoPayload>(AppStartUpState, null, true);
	}

	// Goes to the default landing page; for convenience only
	public abstract void GoToLandingPage(bool preventStackPush = true);

	public abstract string AppStartUpState { get; }

	public class AppStartUpMessage : NoPayloadMessage
	{
	}

	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)
		{
			Messenger.Send(newBindingContextChangeRequestMessage
			{
				Payload = viewModelCreator(),
				PreventNavStackPush = preventStackPush
			});

			return;
		}

		// ELSE create both the page and view model
		var page = pageCreator();
		page.BindingContext = viewModelCreator();

		Messenger.Send(newMainPageChangeRequestMessage
		{
			Payload = page,
			PreventNavStackPush = preventStackPush
		});

		_lastPage = page;
	}
}


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
	};

	private Page _lastPage;

	public override string AppStartUpState => AUTO_SIGN_IN_APP_STATE;

	public override void GoToLandingPage(bool preventStackPush = true)
	{
		GoToAppState<NoPayload>(NO_ANIMAL_APP_STATE, null, preventStackPush);
	}

	// *** WE OVERRIDE AND INSERT OUR BUSNESS LOGIC ***
	protected override void RespondToAppStateChange(string newState, object payload, bool preventStackPush)
	{
		switch (newState)
		{
			case ABOUT_APP_STATE:
				CheckAgainstLastPage(typeof(DummyPage), () => newDummyPage(), () => newAboutViewModel(), preventStackPush);
				break;

			case PREFERENCES_APP_STATE:
				CheckAgainstLastPage(typeof(DummyPage), () => newDummyPage(), () => newPreferencesViewModel(), preventStackPush);
				break;

			case CAT_ANIMAL_APP_STATE:
				CheckAgainstLastPage(typeof(AnimalStage), () => newAnimalStage(), () => newCatViewModel(newCatData()), preventStackPush);
				break;

			case BIRD_ANIMAL_APP_STATE:
				CheckAgainstLastPage(typeof(AnimalStage), () => newAnimalStage(), () => newBirdViewModel(newBirdData()), preventStackPush);
				break;

			case DOG_ANIMAL_APP_STATE:
				CheckAgainstLastPage(typeof(AnimalStage), () => newAnimalStage(), () => newDogViewModel(newDogData()), preventStackPush);
				break;

			default:
				//NO_ANIMAL_APP_STATE:
				CheckAgainstLastPage(typeof(AnimalStage), () => newAnimalStage(), () => newNoAnimalViewModel(), true);
				break;
		}
	}

	private void CheckAgainstLastPage(Type pageType, Func<Page> pageCreator, Func<IViewModelBase> viewModelCreator,
		bool preventStackPush)
	{
		// If the same page, keep it
		if (_lastPage != null && _lastPage.GetType() == pageType)
		{
			Messenger.Send(newBindingContextChangeRequestMessage
			{
				Payload = viewModelCreator(),
				PreventNavStackPush = preventStackPush
			});

			return;
		}

		// ELSE create both the page and view model
		var page = pageCreator();
		page.BindingContext = viewModelCreator();

		Messenger.Send(newMainPageChangeRequestMessage
		{
			Payload = page,
			PreventNavStackPush = preventStackPush
		});

		_lastPage = page;
	}

	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);
	}

	public static int GetMenuOrderFromAppState(string appState)
	{
		return APP_STATES.IndexOf(appState);
	}
}

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(imobject 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;
	}

	// Notify the nav bar directly before the change so it can preserve the existing main page binding context app state
	NavAndMenuBar.OnAppStateChanged(MainPage, messageArgs.PreventNavStackPush);
	MainPage = messageArgs.Payload;

	// IMPORTANT — The assignment above often fails to cause Page.Disappearing for some reason
	if (_lastMainPage != null)
	{
		if (_lastMainPageisIDisposable lastMainPageAsDisposablew)
		{
			lastMainPageAsDisposablew.Dispose();
		}

		_lastMainPage = null;
	}

	_lastMainPage = MainPage;
}



private void BindingContextPageChanged(object sender, BindingContextChangeRequestMessage messageArgs)
{
	if (MainPage != null)
	{
		// Same as with the page; the app state is about to change
		NavAndMenuBar.OnAppStateChanged(MainPage, messageArgs.PreventNavStackPush);
		MainPage.BindingContext = messageArgs.Payload;
	}
}

The down-side of this approach is that we cannot await the page change itself, which can be animated.  The calling code proceeds as if everything is finished.  But the page animation might still be going on for about a second.  That might lead to issues for some users. 

The up-side is that this is a complete decoupled approach to page navigation.  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.

The post The MVVM Framework Anti-Pattern appeared first on Marcus Technical Services.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
GeneralNot just a framework, not just this framework... Pin
Sergey Alexandrovich Kryukov4-Mar-19 21:30
mvaSergey Alexandrovich Kryukov4-Mar-19 21:30 
QuestionNavigation is not a mvvm concern at all PinPopular
Sacha Barber15-Apr-18 21:00
Sacha Barber15-Apr-18 21:00 
AnswerRe: Navigation is not a mvvm concern at all Pin
Kirk Wood16-Apr-18 11:44
Kirk Wood16-Apr-18 11:44 
GeneralRe: Navigation is not a mvvm concern at all Pin
marcusts20-Apr-18 11:13
marcusts20-Apr-18 11:13 
AnswerRe: Navigation is not a mvvm concern at all Pin
André Pereira20-Apr-18 4:27
André Pereira20-Apr-18 4:27 
GeneralRe: Navigation is not a mvvm concern at all Pin
marcusts20-Apr-18 11:25
marcusts20-Apr-18 11:25 
GeneralRe: Navigation is not a mvvm concern at all Pin
André Pereira23-Apr-18 0:09
André Pereira23-Apr-18 0:09 
AnswerRe: Navigation is not a mvvm concern at all Pin
marcusts20-Apr-18 11:25
marcusts20-Apr-18 11:25 
GeneralMVVM and StateMachine Pin
Nick Polideropoulos14-Apr-18 4:25
Nick Polideropoulos14-Apr-18 4:25 
GeneralRe: MVVM and StateMachine Pin
marcusts20-Apr-18 11:18
marcusts20-Apr-18 11:18 
Thanks for the thoughtful remarks!

Feel free to share any code samples that contrast your approach vs. mine. I am as curious as you as to how to best achieve a string codding architecture.
GeneralRe: MVVM and StateMachine Pin
Sergey Alexandrovich Kryukov4-Mar-19 21:49
mvaSergey Alexandrovich Kryukov4-Mar-19 21:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.