ViewModel First - canton7/Stylet GitHub Wiki
The ViewModel-first approach is one that's crucial to Stylet's architecture, but unintuitive if you learnt MVVM in the traditional View-first manner.
Hopefully this article will make everything clear.
Let's start by defining the View-first approach, and what exactly I mean by that. MVVM states that the ViewModel should know nothing about the View, but that View should be aware of the ViewModel. The obvious way to attach a View and ViewModel, then, is have the View construct its ViewModel in its codebehind - something like this:
C# | VB.NET |
public partial class MyView : Window
{
public MyView()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
} |
Partial Public Class MyView : Inherits Window
Public Sub New()
InitializeComponent()
Me.DataContext = new MyViewModel()
End Class |
This is fine. Views can create and own other views, meaning that you can compose your views into a hierarchy. All well and good.
The crunch comes when you've composed a couple of views, say something like this, where a shell contains a top bar and a frame, inside which any page can be shown:
<!-- This is a window which contains a top bar and another page -->
<Window x:Class="MyNamespace.ShellView" ....>
<StackPanel>
<my:TopBarView/>
<Frame x:Name="navigationFrame"/>
</StackPanel>
</Window>
where TopBarView
has its own ViewModel, TopBarViewModel
. Fine.
Now say that TopBarView
has a field containing some data you want to update, for example the title of the current page. Now, the ShellViewModel
knows this (it's decided what the current page is, after all), but the TopBarViewModel
doesn't (how would it? It doesn't really know about anything). The temptation is to expose a dependency property on the TopBarView
and bind it into the ShellViewModel
, like so:
<Window x:Class="MyNamespace.ShellView" .... x:Name="rootObject">
<StackPanel>
<my:TopBarView CurrentPageTitle="{Binding CurrentPageTitle, ElementName=rootObject}"/>
<Frame x:Name="navigationFrame"/>
</StackPanel>
</Window>
but that's just nasty. You've now got one-and-a-bit views bound to the ShellViewModel
.
Another major concern is displaying windows and dialogs. In traditional MVVM, this is a bit of a pain. One option is to instantiate and display the View (using Show()
or ShowDialog()
) from inside the ViewModel (which makes it, or at least that bit of it, untestable). The better option is to instantiate the View you want to display in the codebehind of your View, and show it from there. This means you need to establish ways of telling to View to display this dialog, and a way of getting the results of the dialog back to the ViewModel.
Indeed, setting the content for the Frame
above requires instantiating a View to put in it. This has the same dilemma - either the ViewModel instantiates it (making it untestable), or the View does (leading to communication pain).
Either way, this approach has some nastiness.
The ViewModel-first approach accepts that a ViewModel shouldn't know anything about its View, but does not accept that the View should be responsible for constructing the ViewModel either. Instead, a third service is responsible for locating the correct View for a given ViewModel, and setting up its DataContext correctly.
The default implementation uses naming conventions to locate the correct View for a given ViewModel, replacing 'ViewModel' with 'View' in its name. That's explained in more detail in The ViewManager.
This allows ViewModels to be created by other ViewModels. Which allows ViewModels to know about, and own, other ViewModels. Which allows you to compose your ViewModels properly.
There's another part of this trick, which is best explained by example:
C# | VB.NET |
public class ShellViewModel
{
public TopBarViewModel TopBar { get; private set; }
// Stuff to instantiate and assign TopBarViewModel
} |
Public Class ShellViewModel
Public Property TopBar as TopBarViewModel
' Stuff to instantiate and assign TopBarViewModel
End Class |
<Window x:Class="MyNamespace.ShellView"
xmlns:s="https://github.com/canton7/Stylet" .....>
<StackPanel>
<ContentControl s:View.Model="{Binding TopBar}"/>
<!-- ... -->
</StackPanel>
</Window>
The View.Model
attached property will fetch the ViewModel it's bound to (in this case it's an instance of TopBarViewModel
), and locate the correct view (TopBarView
). It will instantiate an instance, and set it as the content of that ContentControl
.
The upshot is that the TopBarView
can get the name of the current page from its TopBarViewModel
, and the TopBarViewModel
can be told this by the ShellViewModel
. Problem solved!
The ContentControl
trick also works well for navigation:
<Window x:Class="MyNamespace.ShellView"
xmlns:s="https://github.com/canton7/Stylet" .....>
<StackPanel>
<ContentControl s:View.Model="{Binding TopBar}"/>
<ContentControl s:View.Model="{Binding CurrentPage}"/>
</StackPanel>
</Window>
The ShellViewModel
will then navigate to a new page by instantiating a new instance of that page's ViewModel
, and assigning it to the property CurrentPage
. Note how the ShellViewModel
no longer needs to know anything about views. It hasn't had to instantiate a single view. This is a very important, useful, and powerful point.
Dialogs and Windows are handled in much the same way by The WindowManager. This takes a given ViewModel instance, and displays its View as a dialog or window.
With this approach in place, there's nothing you actually need to do in the codebehind. You can of course do so, but there's very little you can't solve with Actions (for handling events), Converters, Attached Properties, and (most importantly) Attached Behaviors.
Stylet lets you delete the codebehind entirely (it will call InitializeComponent
for you), and you are strongly encouraged to do so. Delete the codebehind!
Note: If you're using VB.NET, sometimes your XAML namespaces will stop working if you delete the codebehind. If this is the case, simply recreate the codebehind with the matching filename, give it the correct namespace and class and then leave the remainder blank. For example, MyView.xaml.vb
:
Namespace Views
Public Class MyView
End Class
End Namespace