Thursday, September 7, 2023

How can a ViewModel get access to its View?

Even though the example targets MAUI, the same technique will work for WPF too.

Short Answer is it doesn't. Normally you do not want your ViewModel to contain references to the View. It should be able to execute without a View even existing. However, I was working through one of Microsoft's MAUI walkthroughs and they were referencing color resources defined in the View.

https://learn.microsoft.com/en-us/training/modules/use-shared-resources/5-exercise-use-dynamic-resources-to-update-element

I had tweaked the walkthrough a bit and was using a ViewModel. Normally the Color resources would be defined as properties in the ViewModel and I would bind to them. But I was curious to see how difficult it would be for the ViewModel to access resources defined in the XAML.

    <ContentPage.Resources>
        <Color x:Key="bgColor">#C0C0C0</Color>
        <Color x:Key="fgColor">#0000AD</Color>
    </ContentPage.Resources>

It turns out there's a fairly easy way to do it, even though you should never do it.

You define a public property in the ViewModel and have the View populate it at the appropriate times. The appropriate times are OnLoaded and OnBindingContextChanged.

Here's the property definition in the ViewModel.

    partial class MainViewModel: ObservableObject
    {
        public MainPage mainPage { get; set; }

Here is the View

namespace Notes;
 
public partial class MainPage : ContentPage
{
      public MainPage()
      {
            InitializeComponent();
            this.Loaded += LinkThisToBindingContext;
            this.BindingContextChanged += LinkThisToBindingContext;
      }
 
      private void LinkThisToBindingContext(object sender, EventArgs e)
      {
            (this.BindingContext as MainViewModel).mainPage = this;
      }
}

The View's BindingContext is already populated in the View's constructor so BindingContextChanged is not normally raised after the constructor. I populate the ViewModel in the loaded event too so I have all bases covered.

Here's the XAML

 <?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:Notes"
             x:Class="Notes.MainPage">
    <ContentPage.BindingContext>
        <local:MainViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Resources>
        <Color x:Key="bgColor">#C0C0C0</Color>
        <Color x:Key="fgColor">#0000AD</Color>
    </ContentPage.Resources>
    <Grid RowDefinitions="Auto"
          Background="{DynamicResource bgColor}"
         ColumnDefinitions="*,*"
         Padding="40">
 
        <Button  Margin="5" Text="Dark" Grid.Row="0" Grid.Column="0"
                 Command="{Binding DarkCommand}"
                 TextColor="{DynamicResource fgColor}"
                 BorderColor="{DynamicResource fgColor}"
                 BorderWidth="1"
                 BackgroundColor="{DynamicResource bgColor}"/>
        <Button  Margin="5" Text="Light" Grid.Row="0" Grid.Column="1"
                 Command="{Binding LightCommand}"
                 TextColor="{DynamicResource fgColor}"
                 BorderColor="{DynamicResource fgColor}"
                 BorderWidth="1"
                 BackgroundColor="{DynamicResource bgColor}"/>
    </Grid>
</ContentPage>

Here is the code behind...

namespace Notes;
 
public partial class MainPage : ContentPage
{
      public MainPage()
      {
            InitializeComponent();
            this.Loaded += LinkThisToBindingContext;
            this.BindingContextChanged += LinkThisToBindingContext;
      }
 
      private void LinkThisToBindingContext(object sender, EventArgs e)
      {
            (this.BindingContext as MainViewModel).mainPage = this;
      }
}

and here is the ViewModel.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
 
namespace Notes
{
    partial class MainViewModel: ObservableObject
    {
        public MainPage mainPage { get; set; }
 
        [RelayCommand]
        public void Dark()
        {
            mainPage.Resources["fgColor"] = Colors.Navy;
            mainPage.Resources["bgColor"] = Colors.Silver;
        }
 
        [RelayCommand]
        public void Light()
        {
            mainPage.Resources["fgColor"] = Colors.Silver;
            mainPage.Resources["bgColor"] = Colors.Navy;
        }
 
    }
}

If you run the application you can see the relay commands have access to the resources via the mainPage property. It's really bad architecture, but it can be done.  



No comments:

Post a Comment