Using WPF MVVM

2023年11月2日 58点热度 2人点赞 0条评论
内容目录

The CommunityToolkit.Mvvm package is mainly used for code generation, which helps reduce the amount of code users need to write. It facilitates the implementation of the MVVM design pattern in WPF, thereby lowering code complexity.

To introduce the project package:

    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1 "/>

To learn MVVM, you need to pay attention to the following types:

Now, let’s briefly introduce the usage of commonly used types.

ObservableObject and ObservableProperty

ObservableObject serves as the base class for observable objects that implement the INotifyPropertyChanged and INotifyPropertyChanging interfaces. It can act as a starting point for various objects requiring property change notifications.

The type ObservableProperty is a property that allows observable properties to be generated from annotated fields, significantly reducing the amount of boilerplate code needed to define observable properties.

First, define a ViewModel class.

	// The ViewModel class needs to inherit from ObservableObject
	public partial class DashboardViewModel : ObservableObject
	{
		// Define a field starting with an underscore, using camel case
		[ObservableProperty]
		private int _counter = 0;

		public DashboardViewModel()
		{
			DownloadTextCommand = new AsyncRelayCommand(DownloadText);
		}
	}

Next, define a ViewModel property in your window.

The author demonstrates using a Page mode; readers can also use a Window.

file

		public DashboardViewModel ViewModel { get; }

The property name can be arbitrary; it does not have to be named ViewModel.

Then, bind the property in the xaml file:

        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

We defined _counter, but the MVVM framework automatically generates some code for us.

The following is a simplified example; actual situations are much more complex.

public int Counter
{
    get => _counter;
    set
    {
        if (!EqualityComparer<int?>.Default.Equals(_counter, value))
        {
            OnCounterChanging(value);
            OnCounterChanging(default, value);
            _counter = value;
            OnCounterChanged(value);
            OnCounterChanged(default, value);
        }
    }
}

partial void OnCounterChanging(int value);
partial void OnCounterChanging(int value);

file

MVVM automatically creates the definitions for the step-by-step methods of this property, allowing us to respond to changes in its value.

file

Commands

If you want to trigger an event after clicking a button, in traditional WPF you would need to define a method. Using MVVM, button events can be treated as commands, making the entire process simpler.

You simply define a function in the ViewModel and add the [RelayCommand] attribute:

	public partial class DashboardViewModel : ObservableObject
	{
		// Define a field starting with an underscore, using camel case
		[ObservableProperty]
		private int _counter = 0;

        // Still write private
		[RelayCommand]
		private void OnCounterIncrement()
		{
			// Note: Use the generated property instead of using _counter;
			Counter++;
		}
	}

MVVM will automatically generate a command function named CounterIncrementCommand.

Then bind the command in your xaml:

        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />
        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

file

You can also write it as an asynchronous method.

		[RelayCommand]
		private async Task OnCounterIncrement()
		{
			Counter++;
			await Task.CompletedTask;
		}

Additionally, the RelayCommand attribute can be configured to enable or disable commands or allow concurrency among them.

Moreover, you can manually set up command property binding methods:

	public partial class DashboardViewModel : ObservableObject
	{
		[ObservableProperty]
		private int _counter = 0;

		public DashboardViewModel()
		{
			UpdateCounterCommand = new RelayCommand(UpdateCounter);
		}

		public ICommand UpdateCounterCommand { get; }

		private void UpdateCounter()
		{
			Counter++;
		}
	}
		public DashboardViewModel()
		{
			UpdateCounterCommand = new AsyncRelayCommand(UpdateCounter);
		}

		public ICommand UpdateCounterCommand { get; }

		private async Task UpdateCounter()
		{
			Counter++;
		}

Bind the command in xaml:

        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.UpdateCounterCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />
        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

Property Change Notification

In MVVM, there is a feature called [NotifyPropertyChangedFor]. What is its purpose? The author has researched for a long time without finding much information and can only test it gradually.

[NotifyPropertyChangedFor] helps reduce the amount of code written for notifications.

Typically, WPF controls won't work directly by binding properties.

	public partial class DashboardViewModel : ObservableObject
	{
		[ObservableProperty]
		private int _counter = 0;

		public int MapValue
		{
			get { return _counter; }
		}

		[RelayCommand]
		private void OnCounterIncrement()
		{
			Counter++;
			MapValue++;
		}
        
        /*
        // Or alternatively:
		public int MapValue
		{
			get { return _counter; }
		}

		[RelayCommand]
		private void OnCounterIncrement()
		{
			Counter++;
		}
        */
	}
        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />
        
        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />
        
        <Label Grid.Column="2" Margin="60,0,0,0" Content="{Binding ViewModel.MapValue, Mode=OneWay}" />

file

As can be seen, the label on the right does not update when the value changes.

However, after modifying it to:

	public partial class DashboardViewModel : ObservableObject
	{
		[NotifyPropertyChangedFor(nameof(MapValue))]
		[ObservableProperty]
		private int _counter = 0;

		public int MapValue
		{
			get { return _counter; }
		}

		[RelayCommand]
		private void OnCounterIncrement()
		{
			Counter++;
		}
	}

file

When the Counter property changes, it will notify the bound MapValue property to change as well.

This is suitable for scenarios where MapValue is only used for display and does not need to be modified directly by the UI.

For example, to input a value and compute the square of x:

		[NotifyPropertyChangedFor(nameof(Result))]
		[ObservableProperty]
		private int _x = 0;

		public int Result
		{
			get { return _x * _x; }
		}

When x value changes, it will notify the control to update the Result value by re-invoking get. For Result, set is meaningless.

Multiple properties can also [NotifyPropertyChangedFor] to the same property.

		[NotifyPropertyChangedFor(nameof(MapValue))]
		[ObservableProperty]
		private int _x = 0;

		[NotifyPropertyChangedFor(nameof(MapValue))]
		[ObservableProperty]
		private int _y = 0;

		public int MapValue
		{
			get { return _x * _y; }
		}

When either X or Y changes, the interface will recalculate the result.

Additionally, you can use [NotifyCanExecuteChangedFor] to automatically invoke commands during property changes.

However, the code below will have no effect; TestCommand will not be executed.

	public partial class DashboardViewModel : ObservableObject
	{
		[NotifyCanExecuteChangedFor(nameof(TestCommand))]
		[ObservableProperty]
		private int _counter = 0;


		[RelayCommand]
		private void OnCounterIncrement()
		{
			Counter++;
		}

        // TestCommand =&gt; testCommand ??= new RelayCommand(OnTest);
		[RelayCommand]
		private void OnTest()
		{
			Counter++;
		}

Due to the limited information available from the official website and other sources, the author is unsure how to use it. The official CommunityToolkit / MVVM-Samples repository does not provide information on the usage of [NotifyCanExecuteChangedFor].

However, based on the code generated by MVVM, theoretically, the TestCommand should be executed. Since it's ineffective and there is no resource to consult, it will be left at that.

file

ObservableValidator

A type for model validation. Since ObservableValidator is also an abstract class, it cannot be mixed with ObservableObject.

Define a new ViewModel:

	public partial class FormViewModel: ObservableValidator
	{
		private string name;

		[Required]
		[MinLength(2)]
		[MaxLength(5)]
		public string Name
		{
			get =&gt; name;
			set =&gt; SetProperty(ref name, value, true);
		}

		[RelayCommand]
		private void Check()
		{
			ValidateAllProperties();

			if (HasErrors)
			{
				return;
			}
		}
	}
	public partial class DashboardPage : INavigableView&lt;DashboardViewModel&gt;
	{
		public DashboardViewModel ViewModel { get; }

		public FormViewModel Form { get; }

		public DashboardPage(DashboardViewModel viewModel)
		{
			ViewModel = viewModel;
			DataContext = this;
			Form = new FormViewModel();
			InitializeComponent();
		}
	}

Using it on the UI:

        &lt;ui:Button
            Grid.Column=&quot;0&quot;
            Command=&quot;{Binding Form.CheckCommand, Mode=OneWay}&quot;
            Content=&quot;Click me!&quot;
            Icon=&quot;Fluent24&quot; /&gt;

        &lt;TextBox
            Grid.Column=&quot;2&quot;
            Margin=&quot;12,0,0,0&quot;
            VerticalAlignment=&quot;Center&quot;
            Width=&quot;200&quot;
            Height=&quot;30&quot;
            BorderThickness=&quot;1,1,1,1&quot;
            Background=&quot;Blue&quot;
            Text=&quot;{Binding Form.Name, Mode=TwoWay}&quot; /&gt;

file

If the model validation fails upon clicking, the corresponding input box will have some styles, such as a red border. However, customization of styles is often required, necessitating more extensive work.

痴者工良

高级程序员劝退师

文章评论