.NET WPF CommunityToolkit.Mvvm框架

文章目录

  • [.NET WPF CommunityToolkit.Mvvm框架](#.NET WPF CommunityToolkit.Mvvm框架)
    • [1 源生成器](#1 源生成器)
      • [1.1 ObservablePropertyAttribute & RelayCommandAttribute](#1.1 ObservablePropertyAttribute & RelayCommandAttribute)
      • [1.2 INotifyPropertyChangedAttribute](#1.2 INotifyPropertyChangedAttribute)
    • [2 可观测对象](#2 可观测对象)
      • [2.1 ObservableValidator](#2.1 ObservableValidator)
      • [2.2 ObservableRecipient](#2.2 ObservableRecipient)

.NET WPF CommunityToolkit.Mvvm框架

1 源生成器

1.1 ObservablePropertyAttribute & RelayCommandAttribute

csharp 复制代码
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows;

namespace TestCommunityToolkit._1_Attribute
{
    public partial class BlogVO : ObservableObject
    {
        [property: JsonIgnore]
        [ObservableProperty]
        private string _name;

        // [ObservableProperty] : ObservableProperty 类型是一种允许从带批注字段生成可观察属性的特性。 其用途是显著减少定义可观察属性所需的样本量。它将生成如下所示的可观察属性:
        //
        //         ↓
        //
        // [JsonIgnore]
        // public string? Name
        // {
        //     get => name;
        //     set
        //     {
        //         if (!EqualityComparer<string?>.Default.Equals(name, value))
        //         {
        //             string? oldValue = name;
        //             OnNameChanging(value);
        //             OnNameChanging(oldValue, value);
        //             OnPropertyChanging();
        //             name = value;
        //             OnNameChanged(value);
        //             OnNameChanged(oldValue, value);
        //             OnPropertyChanged();
        //         }
        //     }
        // }

        [ObservableProperty]
        private string _description;

        [ObservableProperty]
        private string _url;
        
        
        [RelayCommand]
        private void BlogInfo()
        {
            MessageBox.Show($"Name: {Name}\nUrl: {Url}\nDescription: {Description}");
        }
        // [RelayCommand] : RelayCommand 类型是一个特性,允许为带批注的方法生成中继命令属性。 其目的是完全消除在 viewmodel 中定义命令包装私有方法所需的模板。它将生成如下所示的命令:
        //
        //       ↓
        //
        // private ICommand blogInfoCommand;
        // public ICommand BlogInfoCommand => blogInfoCommand ??= new RelayCommand(BlogInfo);
        // 将基于方法名称创建生成的命令的名称。 生成器将使用方法名称并在末尾追加"Command",并且去除"On"前缀(如果存在)。 此外,对于异步方法,"Async"后缀也会在追加"Command"之前去除。

        [RelayCommand]
        private async Task AddPost()
        {
            await Task.Delay(1000);
            // Add a new post to the list...
        }
        // [RelayCommand] : RelayCommand 类型是一个特性,允许为带批注的方法生成中继命令属性。 其目的是完全消除在 viewmodel 中定义命令包装私有方法所需的模板。它将生成如下所示的命令:
        //
        //       ↓
        //
        // private ICommand addPostCommand;
        // public IAsyncRelayCommand AddPostCommand => addPostCommand ??= new AsyncRelayCommand(new Func<Task>(AddPost));
        // 将基于方法名称创建生成的命令的名称。 生成器将使用方法名称并在末尾追加"Command",并且去除"On"前缀(如果存在)。 此外,对于异步方法,"Async"后缀也会在追加"Command"之前去除。
    }
}

1.2 INotifyPropertyChangedAttribute

INotifyPropertyChanged 类型是一个允许将 MVVM 支持代码插入现有类型的属性,其目的是在需要这些类型的相同功能,但已经从另一种类型中实现目标类型的情况下,为开发人员提供支持。 由于 C# 不允许多重继承,因此可以转而使用这些属性让 MVVM 工具包生成器将相同的代码直接添加到这些类型中,从而避开此限制。

为了正常工作,带批注的类型需要位于分部类中。 如果对类型进行嵌套,则必须也将声明语法树中的所有类型批注为分部。 否则将导致编译错误,因为生成器无法使用请求的其他代码生成该类型的其他分部声明。
这些属性仅适用于目标类型不能仅从等效类型(例如从 ObservableObject)继承的情况。 如果可能,推荐的方法是继承,因为它将通过避免在最终程序集中创建重复的代码来减小二进制文件大小。

csharp 复制代码
using CommunityToolkit.Mvvm.ComponentModel;

namespace TestCommunityToolkit._1_Attribute
{
    public class BaseObject
    {
        // Some common properties and methods...
    }

    [INotifyPropertyChanged]
    public partial class BlogViewModel : BaseObject
    {
        // Some properties and methods...
    }
}

这将在 MyViewModel 类型中生成一个完整的 INotifyPropertyChanged 实现,并附带可用于降低详细程度的其他帮助程序(如 SetProperty)。 以下是各种属性的简要总结:

INotifyPropertyChanged:实现接口,并添加帮助程序方法来设置属性和引发事件。

ObservableObject:添加 ObservableObject 类型中的所有代码。 它在概念上等同于 INotifyPropertyChanged,主要区别在于它还实现了 INotifyPropertyChanging。

ObservableRecipient:添加 ObservableRecipient 类型中的所有代码。 特别是,可以将其添加到从 ObservableValidator 继承的类型,以合并两者。

csharp 复制代码
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace TestCommunityToolkit._1_Attribute
{
    /// <inheritdoc/>
    partial class BlogViewModel : global::System.ComponentModel.INotifyPropertyChanged
    {
        /// <inheritdoc cref = "global::System.ComponentModel.INotifyPropertyChanged.PropertyChanged"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public event global::System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
        /// <summary>
        /// Raises the <see cref = "PropertyChanged"/> event.
        /// </summary>
        /// <param name = "e">The input <see cref = "global::System.ComponentModel.PropertyChangedEventArgs"/> instance.</param>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected virtual void OnPropertyChanged(global::System.ComponentModel.PropertyChangedEventArgs e)
        {
            PropertyChanged?.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref = "PropertyChanged"/> event.
        /// </summary>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected void OnPropertyChanged([global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            OnPropertyChanged(new global::System.ComponentModel.PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Compares the current and new values for a given property. If the value has changed, updates
        /// the property with the new value, then raises the <see cref = "PropertyChanged"/> event.
        /// </summary>
        /// <typeparam name = "T">The type of the property that changed.</typeparam>
        /// <param name = "field">The field storing the property's value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are the same.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<T>([global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("newValue")] ref T field, T newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(field, newValue))
            {
                return false;
            }

            field = newValue;
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given property. If the value has changed, updates
        /// the property with the new value, then raises the <see cref = "PropertyChanged"/> event.
        /// See additional notes about this overload in <see cref = "SetProperty{T}(ref T, T, string)"/>.
        /// </summary>
        /// <typeparam name = "T">The type of the property that changed.</typeparam>
        /// <param name = "field">The field storing the property's value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "comparer">The <see cref = "global::System.Collections.Generic.IEqualityComparer{T}"/> instance to use to compare the input values.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<T>([global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("newValue")] ref T field, T newValue, global::System.Collections.Generic.IEqualityComparer<T> comparer, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            if (comparer.Equals(field, newValue))
            {
                return false;
            }

            field = newValue;
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given property. If the value has changed, updates
        /// the property with the new value, then raises the <see cref = "PropertyChanged"/> event.
        /// This overload is much less efficient than <see cref = "SetProperty{T}(ref T, T, string)"/> and it
        /// should only be used when the former is not viable (eg. when the target property being
        /// updated does not directly expose a backing field that can be passed by reference).
        /// For performance reasons, it is recommended to use a stateful callback if possible through
        /// the <see cref = "SetProperty{TModel, T}(T, T, TModel, global::System.Action{TModel, T}, string? )"/> whenever possible
        /// instead of this overload, as that will allow the C# compiler to cache the input callback and
        /// reduce the memory allocations. More info on that overload are available in the related XML
        /// docs. This overload is here for completeness and in cases where that is not applicable.
        /// </summary>
        /// <typeparam name = "T">The type of the property that changed.</typeparam>
        /// <param name = "oldValue">The current property value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "callback">A callback to invoke to update the property value.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are the same.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<T>(T oldValue, T newValue, global::System.Action<T> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(oldValue, newValue))
            {
                return false;
            }

            callback(newValue);
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given property. If the value has changed, updates
        /// the property with the new value, then raises the <see cref = "PropertyChanged"/> event.
        /// See additional notes about this overload in <see cref = "SetProperty{T}(T, T, global::System.Action{T}, string)"/>.
        /// </summary>
        /// <typeparam name = "T">The type of the property that changed.</typeparam>
        /// <param name = "oldValue">The current property value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "comparer">The <see cref = "global::System.Collections.Generic.IEqualityComparer{T}"/> instance to use to compare the input values.</param>
        /// <param name = "callback">A callback to invoke to update the property value.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<T>(T oldValue, T newValue, global::System.Collections.Generic.IEqualityComparer<T> comparer, global::System.Action<T> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            if (comparer.Equals(oldValue, newValue))
            {
                return false;
            }

            callback(newValue);
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given nested property. If the value has changed,
        /// updates the property and then raises the <see cref = "PropertyChanged"/> event.
        /// The behavior mirrors that of <see cref = "SetProperty{T}(ref T, T, string)"/>,
        /// with the difference being that this method is used to relay properties from a wrapped model in the
        /// current instance. This type is useful when creating wrapping, bindable objects that operate over
        /// models that lack support for notification (eg. for CRUD operations).
        /// Suppose we have this model (eg. for a database row in a table):
        /// <code>
        /// public class Person
        /// {
        ///     public string Name { get; set; }
        /// }
        /// </code>
        /// We can then use a property to wrap instances of this type into our observable model (which supports
        /// notifications), injecting the notification to the properties of that model, like so:
        /// <code>
        /// [INotifyPropertyChanged]
        /// public partial class BindablePerson
        /// {
        ///     public Model { get; }
        ///
        ///     public BindablePerson(Person model)
        ///     {
        ///         Model = model;
        ///     }
        ///
        ///     public string Name
        ///     {
        ///         get => Model.Name;
        ///         set => Set(Model.Name, value, Model, (model, name) => model.Name = name);
        ///     }
        /// }
        /// </code>
        /// This way we can then use the wrapping object in our application, and all those "proxy" properties will
        /// also raise notifications when changed. Note that this method is not meant to be a replacement for
        /// <see cref = "SetProperty{T}(ref T, T, string)"/>, and it should only be used when relaying properties to a model that
        /// doesn't support notifications, and only if you can't implement notifications to that model directly (eg. by having
        /// it implement <see cref = "global::System.ComponentModel.INotifyPropertyChanged"/>). The syntax relies on passing the target model and a stateless callback
        /// to allow the C# compiler to cache the function, which results in much better performance and no memory usage.
        /// </summary>
        /// <typeparam name = "TModel">The type of model whose property (or field) to set.</typeparam>
        /// <typeparam name = "T">The type of property (or field) to set.</typeparam>
        /// <param name = "oldValue">The current property value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "model">The model containing the property being updated.</param>
        /// <param name = "callback">The callback to invoke to set the target property value, if a change has occurred.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are the same.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, global::System.Action<TModel, T> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
            where TModel : class
        {
            if (global::System.Collections.Generic.EqualityComparer<T>.Default.Equals(oldValue, newValue))
            {
                return false;
            }

            callback(model, newValue);
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given nested property. If the value has changed,
        /// updates the property and then raises the <see cref = "PropertyChanged"/> event.
        /// The behavior mirrors that of <see cref = "SetProperty{T}(ref T, T, string)"/>,
        /// with the difference being that this method is used to relay properties from a wrapped model in the
        /// current instance. See additional notes about this overload in <see cref = "SetProperty{TModel, T}(T, T, TModel, global::System.Action{TModel, T}, string)"/>.
        /// </summary>
        /// <typeparam name = "TModel">The type of model whose property (or field) to set.</typeparam>
        /// <typeparam name = "T">The type of property (or field) to set.</typeparam>
        /// <param name = "oldValue">The current property value.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "comparer">The <see cref = "global::System.Collections.Generic.IEqualityComparer{T}"/> instance to use to compare the input values.</param>
        /// <param name = "model">The model containing the property being updated.</param>
        /// <param name = "callback">The callback to invoke to set the target property value, if a change has occurred.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetProperty<TModel, T>(T oldValue, T newValue, global::System.Collections.Generic.IEqualityComparer<T> comparer, TModel model, global::System.Action<TModel, T> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
            where TModel : class
        {
            if (comparer.Equals(oldValue, newValue))
            {
                return false;
            }

            callback(model, newValue);
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Compares the current and new values for a given field (which should be the backing field for a property).
        /// If the value has changed, updates the field and then raises the <see cref = "PropertyChanged"/> event.
        /// The behavior mirrors that of <see cref = "SetProperty{T}(ref T, T, string)"/>, with the difference being that
        /// this method will also monitor the new value of the property (a generic <see cref = "global::System.Threading.Tasks.Task"/>) and will also
        /// raise the <see cref = "PropertyChanged"/> again for the target property when it completes.
        /// This can be used to update bindings observing that <see cref = "global::System.Threading.Tasks.Task"/> or any of its properties.
        /// This method and its overload specifically rely on the <see cref = "TaskNotifier"/> type, which needs
        /// to be used in the backing field for the target <see cref = "global::System.Threading.Tasks.Task"/> property. The field doesn't need to be
        /// initialized, as this method will take care of doing that automatically. The <see cref = "TaskNotifier"/>
        /// type also includes an implicit operator, so it can be assigned to any <see cref = "global::System.Threading.Tasks.Task"/> instance directly.
        /// Here is a sample property declaration using this method:
        /// <code>
        /// private TaskNotifier myTask;
        ///
        /// public Task MyTask
        /// {
        ///     get => myTask;
        ///     private set => SetAndNotifyOnCompletion(ref myTask, value);
        /// }
        /// </code>
        /// </summary>
        /// <param name = "taskNotifier">The field notifier to modify.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are
        /// the same. The return value being <see langword="true"/> only indicates that the new value being assigned to
        /// <paramref name = "taskNotifier"/> is different than the previous one, and it does not mean the new
        /// <see cref = "global::System.Threading.Tasks.Task"/> instance passed as argument is in any particular state.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier? taskNotifier, global::System.Threading.Tasks.Task? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
        }

        /// <summary>
        /// Compares the current and new values for a given field (which should be the backing field for a property).
        /// If the value has changed, updates the field and then raises the <see cref = "PropertyChanged"/> event.
        /// This method is just like <see cref = "SetPropertyAndNotifyOnCompletion(ref TaskNotifier, global::System.Threading.Tasks.Task, string)"/>,
        /// with the difference being an extra <see cref = "global::System.Action{T}"/> parameter with a callback being invoked
        /// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
        /// </summary>
        /// <param name = "taskNotifier">The field notifier to modify.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "callback">A callback to invoke to update the property value.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are the same.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier? taskNotifier, global::System.Threading.Tasks.Task? newValue, global::System.Action<global::System.Threading.Tasks.Task?> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, callback, propertyName);
        }

        /// <summary>
        /// Compares the current and new values for a given field (which should be the backing field for a property).
        /// If the value has changed, updates the field and then raises the <see cref = "PropertyChanged"/> event.
        /// The behavior mirrors that of <see cref = "SetProperty{T}(ref T, T, string)"/>, with the difference being that
        /// this method will also monitor the new value of the property (a generic <see cref = "global::System.Threading.Tasks.Task"/>) and will also
        /// raise the <see cref = "PropertyChanged"/> again for the target property when it completes.
        /// This can be used to update bindings observing that <see cref = "global::System.Threading.Tasks.Task"/> or any of its properties.
        /// This method and its overload specifically rely on the <see cref = "TaskNotifier{T}"/> type, which needs
        /// to be used in the backing field for the target <see cref = "global::System.Threading.Tasks.Task"/> property. The field doesn't need to be
        /// initialized, as this method will take care of doing that automatically. The <see cref = "TaskNotifier{T}"/>
        /// type also includes an implicit operator, so it can be assigned to any <see cref = "global::System.Threading.Tasks.Task"/> instance directly.
        /// Here is a sample property declaration using this method:
        /// <code>
        /// private TaskNotifier&lt;int&gt; myTask;
        ///
        /// public Task&lt;int&gt; MyTask
        /// {
        ///     get => myTask;
        ///     private set => SetAndNotifyOnCompletion(ref myTask, value);
        /// }
        /// </code>
        /// </summary>
        /// <typeparam name = "T">The type of result for the <see cref = "global::System.Threading.Tasks.Task{TResult}"/> to set and monitor.</typeparam>
        /// <param name = "taskNotifier">The field notifier to modify.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are
        /// the same. The return value being <see langword="true"/> only indicates that the new value being assigned to
        /// <paramref name = "taskNotifier"/> is different than the previous one, and it does not mean the new
        /// <see cref = "global::System.Threading.Tasks.Task{TResult}"/> instance passed as argument is in any particular state.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier<T>? taskNotifier, global::System.Threading.Tasks.Task<T>? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, null, propertyName);
        }

        /// <summary>
        /// Compares the current and new values for a given field (which should be the backing field for a property).
        /// If the value has changed, updates the field and then raises the <see cref = "PropertyChanged"/> event.
        /// This method is just like <see cref = "SetPropertyAndNotifyOnCompletion{T}(ref TaskNotifier{T}, global::System.Threading.Tasks.Task{T}, string)"/>,
        /// with the difference being an extra <see cref = "global::System.Action{T}"/> parameter with a callback being invoked
        /// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
        /// </summary>
        /// <typeparam name = "T">The type of result for the <see cref = "global::System.Threading.Tasks.Task{TResult}"/> to set and monitor.</typeparam>
        /// <param name = "taskNotifier">The field notifier to modify.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "callback">A callback to invoke to update the property value.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        /// <remarks>
        /// The <see cref = "PropertyChanged"/> event is not raised if the current and new value for the target property are the same.
        /// </remarks>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier<T>? taskNotifier, global::System.Threading.Tasks.Task<T>? newValue, global::System.Action<global::System.Threading.Tasks.Task<T>?> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
        {
            return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, callback, propertyName);
        }

        /// <summary>
        /// Implements the notification logic for the related methods.
        /// </summary>
        /// <typeparam name = "TTask">The type of <see cref = "global::System.Threading.Tasks.Task"/> to set and monitor.</typeparam>
        /// <param name = "taskNotifier">The field notifier.</param>
        /// <param name = "newValue">The property's value after the change occurred.</param>
        /// <param name = "callback">(optional) A callback to invoke to update the property value.</param>
        /// <param name = "propertyName">(optional) The name of the property that changed.</param>
        /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, global::System.Action<TTask?>? callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
            where TTask : global::System.Threading.Tasks.Task
        {
            if (ReferenceEquals(taskNotifier.Task, newValue))
            {
                return false;
            }

            bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true;
            taskNotifier.Task = newValue;
            OnPropertyChanged(propertyName);
            if (isAlreadyCompletedOrNull)
            {
                if (callback != null)
                {
                    callback(newValue);
                }

                return true;
            }

            async void MonitorTask()
            {
                await global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__TaskExtensions.GetAwaitableWithoutEndValidation(newValue!);
                if (ReferenceEquals(taskNotifier.Task, newValue))
                {
                    OnPropertyChanged(propertyName);
                }

                if (callback != null)
                {
                    callback(newValue);
                }
            }

            MonitorTask();
            return true;
        }

        /// <summary>
        /// An interface for task notifiers of a specified type.
        /// </summary>
        /// <typeparam name = "TTask">The type of value to store.</typeparam>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        private interface ITaskNotifier<TTask>
            where TTask : global::System.Threading.Tasks.Task
        {
            /// <summary>
            /// Gets or sets the wrapped <typeparamref name = "TTask"/> value.
            /// </summary>
            TTask? Task { get; set; }
        }

        /// <summary>
        /// A wrapping class that can hold a <see cref = "global::System.Threading.Tasks.Task"/> value.
        /// </summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected sealed class TaskNotifier : ITaskNotifier<global::System.Threading.Tasks.Task>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref = "TaskNotifier"/> class.
            /// </summary>
            internal TaskNotifier()
            {
            }

            private global::System.Threading.Tasks.Task? task;
            /// <inheritdoc/>
            global::System.Threading.Tasks.Task? ITaskNotifier<global::System.Threading.Tasks.Task>.Task { get => this.task; set => this.task = value; }

            /// <summary>
            /// Unwraps the <see cref = "global::System.Threading.Tasks.Task"/> value stored in the current instance.
            /// </summary>
            /// <param name = "notifier">The input <see cref = "TaskNotifier{TTask}"/> instance.</param>
            public static implicit operator global::System.Threading.Tasks.Task? (TaskNotifier? notifier)
            {
                return notifier?.task;
            }
        }

        /// <summary>
        /// A wrapping class that can hold a <see cref = "global::System.Threading.Tasks.Task{T}"/> value.
        /// </summary>
        /// <typeparam name = "T">The type of value for the wrapped <see cref = "global::System.Threading.Tasks.Task{T}"/> instance.</typeparam>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "8.3.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCode]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        protected sealed class TaskNotifier<T> : ITaskNotifier<global::System.Threading.Tasks.Task<T>>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref = "TaskNotifier{TTask}"/> class.
            /// </summary>
            internal TaskNotifier()
            {
            }

            private global::System.Threading.Tasks.Task<T>? task;
            /// <inheritdoc/>
            global::System.Threading.Tasks.Task<T>? ITaskNotifier<global::System.Threading.Tasks.Task<T>>.Task { get => this.task; set => this.task = value; }

            /// <summary>
            /// Unwraps the <see cref = "global::System.Threading.Tasks.Task{T}"/> value stored in the current instance.
            /// </summary>
            /// <param name = "notifier">The input <see cref = "TaskNotifier{TTask}"/> instance.</param>
            public static implicit operator global::System.Threading.Tasks.Task<T>? (TaskNotifier<T>? notifier)
            {
                return notifier?.task;
            }
        }
    }
}

2 可观测对象

2.1 ObservableValidator

对象类

csharp 复制代码
public partial class CommentVO : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [MinLength(2)]
    [MaxLength(8)]
    private string _author;
    
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [MinLength(1)]
    [CustomValidation(typeof(CommentVO), nameof(ValidateText))]
    private string _text;
    
    public static ValidationResult ValidateText(string value, ValidationContext context)
    {
        if (value?.Contains("error") == true)
        {
            return new ValidationResult("Text cannot contain the word 'error'");
        }
        return ValidationResult.Success;
    }
    
    [RelayCommand]
    private void SubmitComment()
    {
        this.ValidateAllProperties();
        if (this.HasErrors)
        {
            string errors = string.Join(Environment.NewLine, this.GetErrors().Select(vr => vr.ErrorMessage));
            MessageBox.Show(errors, "Validation errors", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }
        // ...
    }
}

自动生成类:

csharp 复制代码
partial class CommentVO
{
    /// <inheritdoc cref="_author"/>
    [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.3.0.0")]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    [global::System.ComponentModel.DataAnnotations.RequiredAttribute()]
    [global::System.ComponentModel.DataAnnotations.MinLengthAttribute(2)]
    [global::System.ComponentModel.DataAnnotations.MaxLengthAttribute(8)]
    public string Author
    {
        get => _author;
        [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_author")]
        [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
        set
        {
            if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_author, value))
            {
                OnAuthorChanging(value);
                OnAuthorChanging(default, value);
                OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Author);
                _author = value;
                ValidateProperty(value, "Author");
                OnAuthorChanged(value);
                OnAuthorChanged(default, value);
                OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Author);
            }
        }
    }
    
    /// <inheritdoc cref="_text"/>
    [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.3.0.0")]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    [global::System.ComponentModel.DataAnnotations.MinLengthAttribute(1)]
    [global::System.ComponentModel.DataAnnotations.CustomValidationAttribute(typeof(global::TestCommunityToolkit._2_Observable.CommentVO), "ValidateText")]
    public string Text
    {
        get => _text;
        [global::System.Diagnostics.CodeAnalysis.MemberNotNull("_text")]
        [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
        set
        {
            if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(_text, value))
            {
                OnTextChanging(value);
                OnTextChanging(default, value);
                OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Text);
                _text = value;
                ValidateProperty(value, "Text");
                OnTextChanged(value);
                OnTextChanged(default, value);
                OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Text);
            }
        }
    }
}

2.2 ObservableRecipient

csharp 复制代码
public class CommentMessage : RequestMessage<(bool success, string message)>
{
    public CommentVO Comment { get; set; }

    public CommentMessage(CommentVO comment)
    {
        Comment = comment;
    }
}

public partial class PostVO : ObservableRecipient, IRecipient<CommentMessage>
{
    [ObservableProperty]
    private string _title;
    [ObservableProperty]
    private string _content;
    [ObservableProperty]
    private IList<CommentVO> _comments;

    [RelayCommand]
    private void AddComment()
    {
        IsActive = true;
        try
        {
            CommentWindow window = new CommentWindow();
            window.Owner = Application.Current.MainWindow;
            window.DataContext = new CommentVO();
            window.ShowDialog();
        }
        finally
        {
            IsActive = false;
        }
    }

    public void Receive(CommentMessage message)
    {
        this.Comments.Add(message.Comment);
        message.Reply(new() { message = "Comment added successfully!", success = true });
    }
}

public partial class CommentVO : ObservableValidator
{
    // Some common properties and methods...

    [RelayCommand]
    private void SubmitComment()
    {
        // ...
        CommentMessage commentMessage = WeakReferenceMessenger.Default.Send(new CommentMessage(this));
        bool success = commentMessage.Response.success;
        if (!success)
        {
            MessageBox.Show(commentMessage.Response.message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }
        // ...
    }
}
相关推荐
月落.3 小时前
WPF的<ContentControl>控件
wpf
就是有点傻3 小时前
WPF中的依赖属性
开发语言·wpf
wangnaisheng3 小时前
【WPF】把一个Window放在左上角/右上角顶格显示
wpf
月落.4 小时前
WPF中的INotifyPropertyChanged接口
wpf
界面开发小八哥4 小时前
界面控件DevExpress WPF中文教程:Data Grid——卡片视图设置
.net·wpf·界面控件·devexpress·ui开发
平凡シンプル4 小时前
WPF 打包
wpf
VickyJames4 小时前
基于XAML框架和跨平台项目架构设计的深入技术分析
wpf·开源分享·unoplatform·winui3·项目架构
冷眼Σ(-᷅_-᷄๑)7 小时前
WPF缩放动画和平移动画叠加后会发生什么?
wpf·动画