🛠️

WinUI x CommunityToolkit.Mvvm [ObservableObject・ObservableProperty編]

2024/06/10に公開

CommunityToolkit.MvvmNuGetパッケージを使った実装サンプルコードを紹介します。

UI

サンプルコード

SomePageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;

namespace CommunityToolkitMvvmDemo;

public partial class SomePageViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string _firstName = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string _lastName = string.Empty;

    public string FullName => $"{FirstName} {LastName}";
}
SomePage.xaml.cs
using Microsoft.UI.Xaml.Controls;

namespace CommunityToolkitMvvmDemo;

public sealed partial class SomePage : Page
{
    public SomePage()
    {
        InitializeComponent();
    }

    public SomePageViewModel ViewModel { get; } = new();
}
SomePage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<Page
    x:Class="CommunityToolkitMvvmDemo.SomePage"
    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">

    <Page.Resources>
        <x:Double x:Key="Spacing">12</x:Double>
        <Style
            BasedOn="{StaticResource DefaultTextBoxStyle}"
            TargetType="TextBox">
            <Setter Property="Width" Value="200" />
        </Style>
    </Page.Resources>

    <StackPanel
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Spacing="{StaticResource Spacing}">
        <StackPanel
            Orientation="Horizontal"
            Spacing="{StaticResource Spacing}">
            <TextBox
                Header="First Name"
                Text="{x:Bind ViewModel.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox
                Header="Last Name"
                Text="{x:Bind ViewModel.LastName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        <StackPanel>
            <TextBlock Text="Full Name" />
            <TextBlock Text="{x:Bind ViewModel.FullName, Mode=OneWay}" />
        </StackPanel>
    </StackPanel>

</Page>

解説

ObservableObjectの継承

ObservableObjectは抽象クラスであり、

  • INotifyPropertyChanging
  • INotifyPropertyChanged

が既に実装されています。

どうしても他のクラスを継承する必要がある場合、ObservableObject属性を使うことで、CommunityToolkit.Mvvmのソースジェネレーターに同等のコードを生成させることができます。

SomePageViewModel.cs
[ObservableObject]
public partial class SomePageViewModel : SomeBaseClass
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string _firstName = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string _lastName = string.Empty;

    public string FullName { get => $"{FirstName} {LastName}"; }
}

ソースジェネレーターが生成したコードを確認することができます。


CommunityToolkitMvvmDemo.SomePageViewModel.g.cs

partialキーワード

CommunityToolkit.Mvvmのソースジェネレーターにコードを生成してもらうため、対象クラスをpartialクラスにする必要があります。

ObservableProperty属性

privateフィールドにObservableProperty属性を付けることで、CommunityToolkit.MvvmのソースジェネレーターがObservableなプロパティのコードを生成してくれます。

[ObservableProperty]
private string _firstName = string.Empty;

上記のコードでフィールド名_firstName(firstNamem_firstNameのいずれも可)に従って、下記のコード通り、FirstNameプロパティが自動生成されます。

/// <inheritdoc cref="_firstName"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public string FirstName
{
    get => _firstName;
    [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_firstName")]
    set
    {
        if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_firstName, value))
        {
            OnFirstNameChanging(value);
            OnFirstNameChanging(default, value);
            OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FirstName);
            _firstName = value;
            OnFirstNameChanged(value);
            OnFirstNameChanged(default, value);
            OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName);
        }
    }
}

/// <summary>Executes the logic for when <see cref="FirstName"/> is changing.</summary>
/// <param name="value">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="FirstName"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
partial void OnFirstNameChanging(string value);
/// <summary>Executes the logic for when <see cref="FirstName"/> is changing.</summary>
/// <param name="oldValue">The previous property value that is being replaced.</param>
/// <param name="newValue">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="FirstName"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
partial void OnFirstNameChanging(string? oldValue, string newValue);
/// <summary>Executes the logic for when <see cref="FirstName"/> just changed.</summary>
/// <param name="value">The new property value that was set.</param>
/// <remarks>This method is invoked right after the value of <see cref="FirstName"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
partial void OnFirstNameChanged(string value);
/// <summary>Executes the logic for when <see cref="FirstName"/> just changed.</summary>
/// <param name="oldValue">The previous property value that was replaced.</param>
/// <param name="newValue">The new property value that was set.</param>
/// <remarks>This method is invoked right after the value of <see cref="FirstName"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
partial void OnFirstNameChanged(string? oldValue, string newValue);

また、見ての通り、

/// <inheritdoc cref="_firstName"/>

が定義されていますので、

/// <summary>
/// 名
/// </summary>
[ObservableProperty]
private string _firstName = string.Empty;

と記述すれば、FirstNameプロパティにXMLドキュメントが継承されます。

さらに、

  • OnFirstNameChanging
  • OnFirstNameChanging
  • OnFirstNameChanged
  • OnFirstNameChanged

がpartialメソッドとして用意されていますので、必要であれば次のように使うことができます。

[ObservableProperty]
private string _firstName = string.Empty;

partial void OnFirstNameChanged(string? oldValue, string newValue)
{
    System.Diagnostics.Debug.WriteLine($"FirstName changed from {oldValue} to {newValue}");
}

NotifyPropertyChangedFor属性

NotifyPropertyChangedFor属性も使えば、他のプロパティの変更もUIに通知するコードが自動生成されます。

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName = string.Empty;
/// <inheritdoc cref="_firstName"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public string FirstName
{
    get => _firstName;
    [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_firstName")]
    set
    {
        if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_firstName, value))
        {
            OnFirstNameChanging(value);
            OnFirstNameChanging(default, value);
            OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FirstName);
            _firstName = value;
            OnFirstNameChanged(value);
            OnFirstNameChanged(default, value);
            OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName);

            // ↓このコードが追加されます。
            OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName);
        }
    }
}

いかがでしたでしょうか?
CommunityToolkit.MvvmNuGetパッケージを使えば、超強力なソースジェネレーターの力によって

  • ボイラープレートコードの削減
  • MVVMに最適化されたコードの適用

などのメリットがあります。

CommunityToolkit.MvvmNuGetパッケージにはまだまだ魅力的な機能がありますので、
次回以降も紹介する予定です。

https://zenn.dev/andrewkeepcodin/articles/004-community-toolkit-mvvm-relay-command

Happy Coding!😊

GitHubで編集を提案

Discussion