Search code examples
wpfmvvmmvvm-lightwinui-3winui

How do I migrate this MVVM approach in WPF to WinUI?


Historically in WPF when using MVVM (typically done with MVVMLight) I use the approach where I bind a control to something in its view-model. To wit:

XAML:

xmlns:vm="clr-namespace:My.Namespace.ViewModels"

And then declare the data context in XAML using

DataContext="{Binding Main, Source={StaticResource Locator}}"

ViewModel:

public class MainViewModel : BaseViewModel
{
   // Properties, methods, commands etc. here
}

MVVMLight kicked in and provides the ViewModel locator which allowed me to do the following in the App.xaml

<local:ViewModelLocator x:Key="locator"/>

local is defined earlier in the XAML similar to how

My questions are:

  1. What's the way to follow this kind of pattern now with MVVM Toolkit? The Migrating from MvvmLight article glossed over it. All the examples provided are far too complex for what I'm looking for.

  2. Setting a whole Window's Data Context now appears impossible. Do I need to use the x:Bind syntax for each control?

  3. As an extension of 2. above the examples I've seen are all setting the Data Context within the code-behind. Is this now the recommended approach?

I got this approach years ago mainly from reading Josh Smith's website and some of the Reed Copsey's excellent answers on this site like: What is the preferred way to connect viewmodels to their views?


Solution

  • x:Bind does not use DataContext as its source. Let me show you a simple sample code using the CommunitToolkit.Mvvm.

    MainPage.xaml

    <Page
        x:Class="CommunityToolkitMvvmDemo.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="using:CommunityToolkitMvvmDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
        mc:Ignorable="d">
    
        <StackPanel>
            <TextBox
                PlaceholderText="Enter..."
                Text="{x:Bind ViewModel.SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <!--  x:Bind is `OneTime` mode by default.  -->
            <TextBlock Text="{x:Bind ViewModel.SomeText, Mode=OneWay}" />
            <Button
                Command="{x:Bind ViewModel.ClearSomeTextCommand}"
                Content="Clear" />
        </StackPanel>
    
    </Page>
    

    MainPage.xaml.cs

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            InitializeComponent();
        }
    
        public MainPageViewModel ViewModel { get; } = new();
    }
    

    MainPageViewModel.cs

    // This class needs to be `partial` for the source generator.
    public partial class MainPageViewModel : ObservableObject
    {
        // The source generator will create a `SomeText` property for you.
        [ObservableProperty]
        private string _someText = string.Empty;
    
        // The source generator will create a `ClearSomeTextCommand` for you.
        [RelayCommand]
        private void ClearSomeText() => SomeText = string.Empty;
    }
    

    As you can see, instead of using the DataContext, I have a MainPageViewModel property initialized in code-behind.

    If you want to inject the ViewModel, you can just:

    public MainPage(MainPageViewModel viewModel)
    {
        ViewModel = viewModel;
        InitializeComponent();
    }
    

    Just so you know, you can still use Binding just like WPF:

    <StackPanel>
        <TextBox
            PlaceholderText="Enter..."
            Text="{Binding SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Text="{Binding SomeText}" />
        <Button
            Command="{Binding ClearSomeTextCommand}"
            Content="Clear" />
    </StackPanel>
    
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            InitializeComponent();
            DataContext = new MainPageViewModel();
        }
    

    but you should use x:Bind whenever you can for performance and compile-time validation reasons.

    UPDATE

    If you want to set the DataContext in XAML, you can:

    <Page...>
    
        <Page.DataContext>
           <local:MainPageViewModel x:Name="ViewModel" />
       </Page.DataContext>
    
        <StackPanel>
            <TextBox
                PlaceholderText="Enter..."
                Text="{x:Bind ViewModel.SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock Text="{x:Bind ViewModel.SomeText, Mode=OneWay}" />
            <Button
                Command="{x:Bind ViewModel.ClearSomeTextCommand}"
                Content="Clear" />
        </StackPanel>
    </Page>