65.9K
CodeProject 正在变化。 阅读更多。
Home

C# MVVM 工具包演示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (26投票s)

2022 年 5 月 19 日

CPOL

7分钟阅读

viewsIcon

70509

downloadIcon

4083

本文和演示是关于如何开始使用 MVVM Community Toolkit 以及一些自定义的 Message Box 和对话框接口/服务。

新内容: 下载 NET8CsMvvmToolkit.zip

下载 CsMvvmToolkit_CP.zip

引言

本文和演示是关于如何开始使用 MVVM 工具包以及一些自定义的 MessageBox 和对话框接口/服务。

背景

CodeProject 上有很多关于其他 MVVM 框架的文章,但几乎没有关于 WPFMVVM Toolkit 的。所以我开始撰写本文档。

模型、视图视图模型MVVM 模式)是组织或构建代码的好方法,有助于简化、开发和测试(例如单元测试)您的应用程序。

模型保存数据,与应用程序逻辑无关。

视图模型充当模型和视图之间的连接。

视图是用户界面。

我不会详细描述和解释整个演示项目。重点是如何测试其中的一些功能。

Using the Code

MVVM 结构/功能

MVVM Toolkit 来自微软,并且一些其他使用的功能也不是我自己的:来源如“致谢/参考”部分所示。

内容快速概览

  • MVVM Toolkit 和 .NET 4.7.2
    • RelayCommand
    • OnPropertyChanged
    • ObservableRecipientMessengerViewModelBase
    • DependencyInjection(将 MsgBoxDialog 作为服务运行)
    • ObservableCollection(用于“致谢”Listbox
  • Ribbon 菜单
  • 服务/对话框
  • 使用 RelayCommandICommand 将按钮绑定到 ViewModel

安装 MVVM Toolkit

安装 MVVM Toolkit 的 NuGet 包时,它还会安装 6 或 7 个其他包。

对于 DependencyInjection,我们需要安装另一个 NuGet 包

我为 MsgBox 和一些对话框创建了接口/服务。

DependencyInjection 能够独立于 viewmodel 启动以下对话框

  • FontDlgVM
  • MsgBoxService
  • DialogVM
  • OpenFileDlgVM
  • RibbonStatusService
  • SaveAsFileDlgVM
  • SearchDlgVM

这允许使用自定义 Messagebox/对话框以及进行单元测试。

主窗口概念和代码

MainWindow 显示 Ribbon

其下方是 Tabcontrol,其中包含“MVVM Toolkit Testing”和“RichText”选项卡。

MainWindow 类后面的代码是

{
    /// <summary>
    /// Interaktionslogik für MainWindow.xaml
    /// </summary>

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new TestingViewModel();
        }
    }
}

Application

服务/视图模型的注册在 App 的代码后面完成。

public partial class App : Application
    {
        private bool blnReady;
        public App()
        {
            InitializeComponent();
            Exit += (_, __) => OnClosing();
            Startup += Application_Startup;
            try
            {
                Mod_Public.sAppPath = Directory.GetCurrentDirectory();
                Ioc.Default.ConfigureServices(
                    new ServiceCollection()
                    .AddSingleton<IMsgBoxService, MsgBoxService>()
                    .AddSingleton((IDialog)new DialogVM())
                    .AddSingleton((IOpenFileDlgVM)new OpenFileDlgVM())
                    .AddSingleton((ISaveAsFileDlgVM)new SaveAsFileDlgVM())
                    .AddSingleton((IRichTextDlgVM)new RichTextDlgVM())
                    .BuildServiceProvider());
            }
            catch (Exception ex)
            {
                File.AppendAllText(Mod_Public.sAppPath + @"\Log.txt", 
                string.Format("{0}{1}", Environment.NewLine, 
                DateAndTime.Now.ToString() + "; " + ex.ToString()));
                var msgBoxService = Ioc.Default.GetService<IMsgBoxService>();
                msgBoxService.Show("Unexpected error:" + Constants.vbNewLine + 
                Constants.vbNewLine + ex.ToString(), img: MessageBoxImage.Error);
            }
        }
        private void OnClosing()
        {
        }
        private void Application_Startup(object sender, EventArgs e)
        {
            blnReady = true;
        }
   }

Mod_Public

Mod_Public 包括

   public static void ErrHandler(string sErr)

   public static string ReadTextLines(string FileName)

MVVM 模式 - 详情

从 WPF Ribbon 的角度来看,数据结构/模型很简单

Ribbon 具有菜单项/Ribbon 按钮,它们与 ActiveRichTextBoxActiveTextBox 一起工作。

这就是我们在模型TextData 中看到的内容。

名为 TextData 的模型类

using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;

namespace CsMvvmToolkit_CP
{

    public class TextData : ObservableRecipient, INotifyPropertyChanged
    {
        private string _text;
        private string _richText;
        private RibbonTextBox _NotifyTest;
        private string _readText;
        private TextBox _ActiveTextBox;
        private RichTextBox __ActiveRichTextBox;

        private RichTextBox _ActiveRichTextBox
        {
            [MethodImpl(MethodImplOptions.Synchronized)]
            get { return __ActiveRichTextBox; }

            [MethodImpl(MethodImplOptions.Synchronized)]
            set { __ActiveRichTextBox = value; }
        }

        private Ribbon _MyRibbonWPF;
        private MainWindow _MyMainWindow;

        public TextData()
        {
            // 
        }

        public MainWindow MyMainWindow
        {
            get { return _MyMainWindow; }
            set { _MyMainWindow = (MainWindow)value; }
        }

        public Ribbon MyRibbonWPF
        {
            get { return _MyRibbonWPF; }
            set { _MyRibbonWPF = value; }
        }

        public RibbonTextBox NotifyTestbox
        {
            get { return _NotifyTest; }
            set { _NotifyTest = value; }
        }

        public string RichText
        {
            get { return _richText; }
            set
            {
                _richText = value;
                OnPropertyChanged("RichText");
            }
        }

        public string GetText
        {
            get { return _text; }
            set
            {
                _text = value;
                OnPropertyChanged("GetText");
            }
        }

        public string ReadText
        {
            get { return _readText; }
            set
            {
                _readText = value;
                GetText = _readText;
                OnPropertyChanged("ReadText");
            }
        }

        public TextBox ActiveTextBox
        {
            get { return _ActiveTextBox; }
            set
            {
                _ActiveTextBox = value;
                OnPropertyChanged("ActiveTextBox");
            }
        }

        public RichTextBox ActiveRichTextBox
        {
            get { return _ActiveRichTextBox; }
            set { _ActiveRichTextBox = value; }
        }
    }
}

名为 TestingViewModelViewModel 类

名为 TestingViewModel 的类包含用于测试某些 MVVM 功能的属性、RelayCommandsICommands 和方法。它还包含用于 ObservableCollection(Credits)的代码,该集合用于显示本篇文章的“参考/致谢”的 Listview

整合 - WPF 概念和代码

QAT (QuickAccessToolbar)

您可以从 QAT(右键单击时会出现一个上下文菜单)中移除按钮。您可以将 QAT 显示在 Ribbon下方。您也可以从“设置”选项卡恢复 QAT。您还可以更改 Ribbon背景颜色

DependencyInjection 或 ServiceInjection

如前所述,App 后面的代码中有一些用于此的代码。

带有 ISaveAsFileDlgVM 的保存文件对话框示例

它使用了 interface ISaveAsFileDlgVMservice/viewmodel SaveAsFileDlgVM

public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged

...    
       public ICommand SaveAsFileDlgCommand { get; set; }
...  
       RelayCommand cmdSAFD = new RelayCommand(SaveAsFileDialog);
       SaveAsFileDlgCommand = cmdSAFD;
...
        private void SaveAsFileDialog()
        {
            var dialog = Ioc.Default.GetService<ISaveAsFileDlgVM>();
            if (ActiveRichTextBox is object)
            {
                dialog.SaveAsFileDlg(_textData.RichText, ActiveRichTextBox);
            }
            if (ActiveTextBox is object)
            {
                dialog.SaveAsFileDlg(_textData.GetText, ActiveTextBox);
            }
        }
...

并且,非常重要,XAML 文件中的Command="{Binding SaveAsFileDlgCommand}"/>

<RibbonButton x:Name="SaveAs" Content="RibbonButton" 
 HorizontalAlignment="Left" Height="Auto"
 Margin="94,24,-162,-70" VerticalAlignment="Top" Width="80" Label=" Save As"  KeyTip="S"
 AutomationProperties.AccessKey="S" AutomationProperties.AcceleratorKey="S"
 SmallImageSource="Images/save16.png" CanAddToQuickAccessToolBarDirectly="False"
 ="Save As" Command="{Binding SaveAsFileDlgCommand}"/>

Ribbon,您可以通过以下方式启动和测试其他对话框或 messagebox

  • 打开对话框
  • 搜索(来源如“致谢/参考”部分所示)
  • OpenFileDialog
  • 选项卡“帮助”>“关于”
  • 字体对话框

Messenger 测试

重要的是要添加 Inherits ObservableRecipient,这和其他细节已在 ObservableObject - Windows Community Toolkit | Microsoft Docs 中进行了描述。

"视图特定的消息应在视图的 Loaded 事件中注册,并在 Unloaded 事件中注销,以防止内存泄漏和多个回调注册问题。"

我们可以从 Class TestingViewModel 发送 Msg

Imports Microsoft.Toolkit.Mvvm.Messaging

    public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged

private string msg;
…

    _cmdMsg = new Command(SendMsgRibbonButton_Click);
…

        public ICommand SendMsg
        {
            get { return _cmdMsg; }
        }
…       

        private void SendMsgRibbonButton_Click()
        {
            try
            {
                // DataExchange / Messenger
                string msg = "Test Msg...";
                SetStatus("TestingViewModel", msg);
            }
            catch (Exception ex)
            {
                SetStatus("TestingViewModel", ex.ToString());
                Mod_Public.ErrHandler(ex.ToString());
            }
        }

...
        public void SetStatus(string r, string m)
        {
            try
            {
                Messenger.Send(new DialogMessage(m));
            }
            catch (Exception ex)
            {
                SetStatus("TestingViewModel", ex.ToString());
                Mod_Public.ErrHandler(ex.ToString());
            }
        }
...

    public class StatusMessage
    {
        public StatusMessage(string status)
        {
            NewStatus = status;
        }
        public string NewStatus { get; set; }
    }

仅当消息已注册时,才能发送 Msg

using Microsoft.Toolkit.Mvvm.Messaging;
...
Messenger.Register<DialogMessage>(this, (r, m) => DialogMessage = m.NewStatus);
Messenger.Register<StatusMessage>(this, (r, m) => StatusBarMessage = m.NewStatus);
...
Messenger.Unregister<StatusMessage>(this);
Messenger.Unregister<DialogMessage>(this);

关闭 viewmodel 时,我们必须取消注册消息。

消息会显示在 StatusBarRibbon 上。

PropertyChanged 测试

<RibbonTextBox x:Name="ribbonTextBox" 
 Text="{Binding OnPropertyChangedTest, UpdateSourceTrigger=PropertyChanged}" 
       HorizontalAlignment="Right" Margin="0,0,-90,-30" 
       TextWrapping="Wrap" VerticalAlignment="Bottom" 
       Width="120" UndoLimit="10" FontSize="12"/>
<RibbonTextBox x:Name="NotifyTextBox" Text="{Binding OnPropertyChangedTest, 
 UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Right" Margin="0,0,-90,-53" 
       TextWrapping="Wrap" VerticalAlignment="Bottom" Width="120" 
       UndoLimit="10" FontSize="12"/>

通常,两个 textbox 只在 activeTextbox 与“RichText”或“PlainText”相关时显示。但如果您手动编辑上面的 textbox,您会立即看到下面的 textbox 内容发生了变化。

这是由 XAML 文件中的 UpdateSourceTrigger=PropertyChanged 引起的。

EventTrigger

要求: Microsoft.Xaml.Behaviors.Wpf (NuGet 包)

xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
...
    <b:Interaction.Triggers>
         <b:EventTrigger EventName= "MouseWheel">
             <b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
                 CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
         </b:EventTrigger>
         <b:EventTrigger EventName= "MouseDoubleClick">
             <b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
                 CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
         </b:EventTrigger>
         <b:EventTrigger EventName= "TextChanged">
             <b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
                 CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
         </b:EventTrigger>
         <b:EventTrigger EventName= "MouseEnter">
             <b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
                 CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
         </b:EventTrigger>
     </b:Interaction.Triggers>
...

Ribbon 通过 ContextMenu 最小化以及用于其他事物时,会使用此功能。

ObservableCollection

它是 viewmodel TestingViewModel 的一部分,用于显示“致谢/参考”的 listbox

public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged

#Region " fields"
...
private ObservableCollection<Credits> _credit = new ObservableCollection<Credits>();
...
#End Region
...

    _credit = new ObservableCollection<Credits>()
                {
                    new Credits()
                    {
                        Item = "MVVM Toolkit",
                        Note = "Microsoft",
                        Link = "https://docs.microsoft.com/en-us/windows/
                                communitytoolkit/mvvm/introduction"
                    },
                    new Credits()
                    {
                        Item = "MVVMLight",
                        Note = "GalaSoft",
                        Link = "https://codeproject.org.cn/Articles/768427/
                                The-big-MVVM-Template"
                    },
                    new Credits()
                    {
                        Item = "ICommand with MVVM pattern",
                        Note = "CPOL",
                        Link = "https://codeproject.org.cn/Articles/863671/
                                Using-ICommand-with-MVVM-pattern"
                    },
                    new Credits()
                    {
                        Item = "C# WPF WYSIWYG HTML Editor - CodeProject",
                        Note = "CPOL",
                        Link = "https://codeproject.org.cn/Tips/870549/
                                Csharp-WPF-WYSIWYG-HTML-Editor"
                    },
                    new Credits()
                    {
                        Item = "SearchDialog",
                        Note = "Forum Msg",
                        Link = "https://social.msdn.microsoft.com/forums/vstudio/en-US/
                        fc46affc-9dc9-4a8f-b845-89a024b263bc/
                        how-to-find-and-replace-words-in-wpf-richtextbox"
                    }
               };
...
    public class Credits
    {
        public string Item { get; set; }
        public string Note { get; set; }
        public string Link { get; set; }
}

使用 ObservableCollection 进行测试

点击“清空列表框”以删除致谢信息。

将 XML 读取到 Listbox 会恢复参考文献。

ObservableCollection 的优点是,我们不需要为 Listbox 设置 UpdateTrigger

RichText

在“RichText”选项卡中,您可以选择 RichTextBox 中的文本,并使用 RibbonButton 进行格式化。其中许多是 EditingCommands,仅出现在 UserCtlRibbonWPF.xaml 文件中。

RelayCommands 替换了 2.2 版的一些 ICommands

#region  RelayCommands

   NewFile = new RelayCommand(New_Click);
   ExitApp = new RelayCommand(Exit_Click);
   Print = new RelayCommand(Print_Click);
   Info = new RelayCommand(Info_Click);
   GreenBackground = 
       new RelayCommand(BackgroundGreenRibbonButton_Click);
   WhiteBackground = 
       new RelayCommand(BackgroundWhiteRibbonButton_Click);
   RestoreQAT = new RelayCommand(RestoreQAT_Click);
   Apploaded = new RelayCommand(App_Loaded);
   ClearListbox = new RelayCommand(ClearListboxButton_Click);
   SaveXml = new RelayCommand(SaveXml_Click);
   ReadXml = new RelayCommand(ReadXml_Click);
   ReadLog = new RelayCommand(ReadLog_Click);
   SendMsg = new RelayCommand(SendMsgRibbonButton_Click);
   GetError = new RelayCommand(GetErrorButton_Click);

#endregion

CommunityToolkitRelayCommand 比我以前使用 ICommand 的版本代码更简洁。下面是一个例子

...    
    public IRelayCommand NewFile { get; }
...    
    NewFile = new RelayCommand(New_Click);
...

    private void New_Click()
    {
        try
        {
            if (ActiveRichTextBox is object)
            {
                ActiveRichTextBox.Document.Blocks.Clear();
            }

            if (ActiveTextBox is object)
            {
                GetText = Constants.vbNullString;
            }
        }
...

==========================================

升级NET8,使用项目 NET8CsMvvmToolkit 版本 1.1

使用 Source Generator 的 RelayCommands 的可能性

以下描述基于MS Learn 链接

MVVM source generators - Community Toolkits for .NET | Microsoft Learn:

从 8.0 版开始,MVVM Toolkit 包含全新的 Roslyn 源生成器,这些生成器将大大减少编写 MVVM 架构代码时的样板代码。它们可以简化设置可观察属性、命令等场景。如果您不熟悉源生成器,可以在此处了解更多信息。
 

这在 .Net Framework 4.8 中是不可能的,这就是为什么我创建了这个项目的 NET8 版本。

这是一个旧版本和一个新版本的 Relay Command 的源版本

OLD:         
        public IRelayCommand<RichTextBox> RTBoxCommand { get; }

        RTBoxCommand = new RelayCommand<RichTextBox>(DoParameterisedCommand);

        private void DoParameterisedCommand(object parameter)
        {
            _textData.ActiveRichTextBox = (RichTextBox)parameter;
            _textData.ActiveTextBox = null;
            OnPropertyChangedTest = "RichText";
        }

NEW:

        [RelayCommand]
        private void ParameterRichTBox(object parameter)
        {
            _textData.ActiveRichTextBox = (RichTextBox)parameter;
            _textData.ActiveTextBox = null;
            OnPropertyChangedTest = "RichText";
        }

您可以看到,命令的两行可以被禁用并替换为 [RelayCommand].

为了更好地阅读,我已经将方法名从 DoParameterisedCommand 重命名为 ParameterRichTBox

命令由源生成器创建并保存在某个地方

.../users/{UserName}/AppData/Local/VSGeneratedDocuments/...

或者在项目子文件夹中

\obj\net8.0\generated\.....

在我的项目中,我们有三种类型的命令需要更新。

普通命令、传递参数的命令和启动对话框的命令。

 

更多信息来自 RelayCommand attribute - Community Toolkits for .NET | Microsoft Learn

为了工作,带注释的方法必须位于一个partial 类中。如果类型是嵌套的,则声明语法树中的所有类型也必须注释为 partial。否则将导致编译错误,因为生成器将无法生成该类型的另一个 partial 声明以及所需的命令。

生成的命令名称将基于方法名称创建。生成器将使用方法名称并在末尾附加“Command”,并删除“On”前缀(如果存在)。此外,对于异步方法,在附加“Command”之前也会删除“Async”后缀。

结论

这只是一个演示 - 它还没有准备好用于生产。

但我认为 MVVM Community Toolkit 将为您提供多种扩展。

最后说明:我对任何形式的反馈都非常感兴趣 - 问题、建议和其他。

致谢/参考

历史

  • 2024 年 5 月 16 日 - 添加新章节:升级NET8,使用项目 NET8CsMvvmToolkit
  • 2024 年 5 月 7 日 - 版本 2.2 - Relay Commands 替换了一些 ICommands
  • 2024 年 5 月 6 日 - 版本 2.1 - 重构了 TextDataTestingViewModel
  • 2023 年 2 月 23 日 - 版本 1.1 - 由于 Microsoft.ToolKit.Mvvm 已弃用,我们现在必须使用此替代包:CommunityToolkit.Mvvm
  • 2022 年 6 月 8 日 - 添加了模型、视图视图模型MVVM 模式)解释
  • 2022 年 5 月 19 日 - 初始提交
© . All rights reserved.