WPF 入门笔记 - 07 - MVVM示例

这篇具有很好参考价值的文章主要介绍了WPF 入门笔记 - 07 - MVVM示例。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

滴咚,大家好久不见💖。好就没写东西了,鸽着鸽着就无了😭。。。

回到正题,上篇文章说完命令提了一嘴MVVM模式直接就上MVVMLight这些程序的框架了,虽然也没说多少,但还是有点不好过渡,这篇对MVVM做一些系统学习,同时复习一下前几篇博文中的内容,完成一个采用MVVM模式的示例Demo

🎶 小插曲

N早之前文章中的可能混淆的内容做下说明 - 关于WPF 入门笔记 - 04 - 数据绑定 - OneWayToSource 文本框爆红现象:

WPF 入门笔记 - 07 - MVVM示例
  • 在那篇文章一开始给初值是没问题的,因为OneWayToSource是将数据从绑定目标传递到绑定源,所以不给初值一定会出现绑定失败的问题
  • 文本框爆红我觉得和绑定模式没有关系,我猜测和文本框绑定的数据类型有关,假设文本框绑定的是支持为空的string类型的字符串,它是空的话是不会爆红的,因为空会转化成"",但如果绑定的数据类型是不能为空的int类型,就会在文本框没有内容时出现爆红现象,同时会出现绑定失败

应该是这样,不对的话记得在评论区实现INotifyPropertyChanged接口教教我噢🛫

🎨 什么是MVVM

MVVMModel-View-ViewModel 的缩写,是一种用于构建用户界面的设计模式,由微软 WPFSilverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。它在 WPF 和其他 XAML-based 技术中广泛应用,比如UWP、采用XAMLMAUI APP,在前端开发也比较常见。MVVM 的目标是实现用户界面和业务逻辑之间的彻底分离,以便更好地管理和维护应用程序,并提供更好的可测试性和可扩展性。

MVVM 模式包含以下三个核心组件:

  1. Model(模型)Model 代表应用程序的数据和业务逻辑。它负责存储和管理数据,并且通常不包含任何与用户界面相关的代码。Model 可以是数据实体、数据库访问类、服务类等。
  2. View(视图)View 是用户界面的可视化表示,通常由 XAML 文件定义。它负责展示数据给用户,并捕获用户输入。View 并不处理业务逻辑,而是通过绑定和命令将用户操作传递给 ViewModel
  3. ViewModel(视图模型)ViewModelViewModel 之间的桥梁,负责处理 ViewModel 之间的交互。它将 Model 中的数据转换为 View 所需的数据,并处理用户输入、命令和其他交互逻辑。ViewModel 通常实现了 INotifyPropertyChanged 接口,用于通知 View 当数据发生变化时进行更新。

WPF 入门笔记 - 07 - MVVM示例

MVVM 的基本原则是让 ViewModel 完全解耦,它们之间通过 ViewModel 进行通信。View 通过数据绑定将 ViewModel 中的数据展示给用户,通过命令绑定将用户的操作传递给 ViewModelViewModel 则通过数据绑定和命令绑定将用户输入转换为对 Model 的操作,从而实现业务逻辑的处理。

回到 WPF 里,View就是用xaml描绘的页面,负责与用户交互,响应用户操作,展示数据给用户;ViewModel负责收集需要绑定的数据和命令,通过 ViewDataContext 属性绑定到 View,同时处理UI逻辑;Model 就作为系统中的对象,包含数据对象。

一个 View 对应一个 ViewModel ,一个 ViewModel 可以聚合多个 ModelViewModel 可以对应多个 View,在 MVVM 下,所有事件和动作都转换成命令,如按钮的点击操作,不在是触发点击事件,而是绑定到命令,由命令去执行对应的逻辑。

MVVM 模式的优势包括:

  1. 分离关注点:MVVM 将用户界面逻辑和业务逻辑彻底分开,使代码结构更清晰,易于理解和维护。
  2. 可测试性:由于 ViewViewModel 之间的解耦,可以更轻松地对 ViewModel 进行单元测试,而无需涉及到用户界面的操作。
  3. 可扩展性:MVVM 支持模块化开发,不同的 ViewViewModel 可以独立开发和组合,使应用程序更易于扩展。
  4. 可重用性:ViewModel 可以与不同的 View 进行重用,从而减少了重复编写代码的工作。

🧵 简单MVVM

学习一下 MSDN - .NET MAUI 数据绑定和 MVVM 中的 简单MVVM 章节,是一个.NET MAUIContentPage,用来显示时间,MauiWPF 在命名空间上是有点不一样的:

Part 1

<Window x:Class="OneShotDateTimeDemo.MainWindow"
        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="clr-namespace:OneShotDateTimeDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="One-Shot DateTime Page"
        Width="800"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" 
                    DataContext="{x:Static sys:DateTime.Now}">
            <TextBlock Text="{Binding Year, StringFormat='The year is {0}'}" />
            <TextBlock Text="{Binding StringFormat='The month is {0:MMMM}'}" />
            <TextBlock Text="{Binding Day, StringFormat='The day is {0}'}" />
            <TextBlock Text="{Binding StringFormat='The time is {0:T}'}" />
        </StackPanel>
    </Grid>
</Window>

这段 XamlStackPanelDataContext 中设置了一个绑定到当前日期和时间的 DateTime 对象,然后通过数据绑定将 DateTime 对象的各个属性显示在不同的子元素 TextBlock 中,同时对显示内容进行了格式化。

运行程序,文本中的日期和时间只会在构造和初始化的时候设置一次,不会随时间的变化而变化。

示例中是Label,我这里换成了TextBlock,因为LabelBinding中使用StringFormat不生效【哦,我用的方法不对,Label不能用StringFormat😅】:

Part 2

上面的页面写出来显然是没有意义的,我们想让它实时显示当前时间,这就需要有个东西在时间发生变化时一直更新页面上的时间,这个过程一般会通过后台代码完成,当然可以通过一个 Timer 搞定,但就和这篇文章的内容没关系了,用 MVVM 的模式怎么实现呢?

首先从 MVVM 三部分来看,View 就是标题为 One-Shot DateTime Page 的窗体,ModelDateTime 对象,需要 ViewModel 来处理时间数据同时通知页面响应更改。

创建一个ClockViewModel.cs类:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;

namespace OneShotDateTimeDemo
{
    public class ClockViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private DateTime _dateTime;
        private Timer _timer;

        public DateTime DateTime
        {
            get => _dateTime;
            set
            {
                if (_dateTime != value)
                {
                    _dateTime = value;
                    OnPropertyChanged();
                }
            }
        }

        public ClockViewModel()
        {
            DateTime = DateTime.Now;
            _timer = new Timer(new TimerCallback((s) => DateTime = DateTime.Now),
                               null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
        }

        ~ClockViewModel() =>
            _timer.Dispose();

        public void OnPropertyChanged([CallerMemberName] string name = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

ClockViewModel 类实现了 INotifyPropertyChanged 接口,并通过 Timer 定时更新时间并触发属性更改通知,提供实时的系统时间。完成 ViewModel 部分以后,将 ViewModel 绑定到视图 View - MainWindow 上:

<Window x:Class="OneShotDateTimeDemo.MainWindow"
        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="clr-namespace:OneShotDateTimeDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="One-Shot DateTime Page"
        Width="800"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">

    <Window.DataContext>
        <local:ClockViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" 
                    DataContext="{x:Static sys:DateTime.Now}">
            <TextBlock Text="{Binding Year, StringFormat='The year is {0}'}" />
            <TextBlock Text="{Binding StringFormat='The month is {0:MMMM}'}" />
            <TextBlock Text="{Binding Day, StringFormat='The day is {0}'}" />
            <TextBlock Text="{Binding StringFormat='The time is {0:T}'}" />
        </StackPanel>

        <StackPanel Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="{Binding DateTime, StringFormat='The time is {0:T}'}" />
        </StackPanel>
    </Grid>
</Window>

这样就可以得到一个 MVVM 模式下的时钟了:

WPF 入门笔记 - 07 - MVVM示例

以上就是一个简单的MVVM的示例,功能比较简单,只涉及到了属性变动,无关乎用户操作。【时间更新肉眼可见的有问题👻】

🔮 完整MVVM

接下来完成一个完整的MVVM示例,内容参考自博主丑萌气质狗在B站发布的WPF入门视频,大家可以取搜一下找来看看。内容比较基础,但同时讲解也非常细腻,碎碎念教学,哈哈哈。有些基础但不多的小伙伴可以去看一下😇

登录页面UI

回到正题,示例是一个简单的登录页面的MVVM实现,用界面部分来回顾下之前学习的内容:

MainView.xaml -为了方便区分每部分扮演的角色,给MainWindow.xaml改了个名:

WPF 入门笔记 - 07 - MVVM示例

登录页面是这样设计的,窗体取消了默认的标题栏,调整为带阴影的圆角窗体,左侧放一张登录背景图,右边自绘了一个关闭按钮,文本框和按钮也做了美化。快速来看一下如何实现的。

窗体设置

窗口样式调整为None,不允许调整窗口大小,用Border包裹整个窗体实现圆角以及阴影效果,整体分两部分 - 左侧图片、右侧背景图:

<Window x:Class="MVVMDemo.MainView"
        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="clr-namespace:MVVMDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainView"
        Width="800"
        Height="450"
        AllowsTransparency="True"
        Background="{x:Null}"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None"
        mc:Ignorable="d">
    <Border Margin="5" Background="AntiqueWhite" CornerRadius="8">
        <Border.Effect>
            <DropShadowEffect BlurRadius="5"
                              Direction="0"
                              Opacity="0.3"
                              ShadowDepth="0"
                              Color="Gray" />
        </Border.Effect>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="6*" />
                <ColumnDefinition Width="4*" />
            </Grid.ColumnDefinitions>
            <Border CornerRadius="8,0,0,8">
                <Border.Background>
                    <ImageBrush ImageSource="./Assets/Images/LoginBackground.png" Stretch="Fill" />
                </Border.Background>
            </Border>
            <Border Grid.Column="1" CornerRadius="0,8,0,8">
               ...	// 内容部分
            </Border>
        </Grid>
    </Border>
</Window>
字体图标

之前笔记中有说,这里复习以下。去阿里巴巴矢量图标库选好要用的图标,添加入库,然后从库里添加至项目:

WPF 入门笔记 - 07 - MVVM示例

下载到本地:

WPF 入门笔记 - 07 - MVVM示例WPF 入门笔记 - 07 - MVVM示例

主要用上面勾选的两个文件,html文件是下载的图标演示,tff是程序中用到的字体文件了,把它放到新建的Assets资源文件夹下边:

WPF 入门笔记 - 07 - MVVM示例

之后就可以用了,Text内容可以从演示的html中找到,也可以在矢量图项目里面复制代码:

<TextBox Text="&#xe6a4;" FontFamily="./Assets/Fonts/#iconfont"/>

WPF 入门笔记 - 07 - MVVM示例

控件模板

模板内容因为只在这一个登录窗体上使用,就都定义在车Window.Resources窗体资源里了,所以看上去页面内容有点多,大家也可以单独定义资源文件。先说按钮,两个按钮都是通过自定义控件模板来完成的,关闭按钮主要是定义了一个 Path 控件,用于绘制一个 "X" 形状,类似于关闭按钮的图标,等率按钮就直接用 TextBlock 代替的:

<ControlTemplate x:Key="CloseButton" TargetType="Button">
    <Border Name="back" Background="Transparent" CornerRadius="0,8,0,0">
        <Path HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Data="M0 0 12 12M0 12 12 0"
              Stroke="Black"
              StrokeThickness="1" />
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter TargetName="back" Property="Background" Value="red" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<ControlTemplate x:Key="LoginButton" TargetType="Button">
    <Border Name="back" Background="{TemplateBinding Background}" CornerRadius="8">
        <TextBlock Text="登录" VerticalAlignment="Center" HorizontalAlignment="Center"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter TargetName="back" Property="Background" Value="red"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

文本框也是通过自定义控件模板来完成的,只不过自己写东西有点多,所以是通过编辑模板给文本库框加的图标:

WPF 入门笔记 - 07 - MVVM示例

WPF 入门笔记 - 07 - MVVM示例

WPF 入门笔记 - 07 - MVVM示例

<Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 	BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True" CornerRadius="8">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*"/>
            <ColumnDefinition Width="8*"/>
        </Grid.ColumnDefinitions>
        <TextBlock FontFamily="/Assets/Fonts/#iconfont" Text="{TemplateBinding Tag}" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
        <ScrollViewer Grid.Column="1" x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" VerticalAlignment="Center"/>
    </Grid>
</Border>

图标属性因为Text属性要获取输入的文本,所以改成了通过文本框的 Tag 属性绑定内容。

而后的控件布局就不解释了吧,直接看代码吧:

MainView.xaml
<Window x:Class="MVVMDemo.MainView"
        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="clr-namespace:MVVMDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainView"
        Width="800"
        Height="450"
        AllowsTransparency="True"
        Background="{x:Null}"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None"
        mc:Ignorable="d">
    <Window.Resources>
        <ControlTemplate x:Key="CloseButton" TargetType="Button">
            <Border Name="back" Background="Transparent" CornerRadius="0,8,0,0">
                <Path HorizontalAlignment="Center"
                      VerticalAlignment="Center"
                      Data="M0 0 12 12M0 12 12 0"
                      Stroke="Black"
                      StrokeThickness="1" />
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter TargetName="back" Property="Background" Value="red" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <ControlTemplate x:Key="LoginButton" TargetType="Button">
            <Border Name="back" Background="{TemplateBinding Background}" CornerRadius="8">
                <TextBlock Text="登录" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter TargetName="back" Property="Background" Value="red"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <SolidColorBrush x:Key="TextBox.Static.Border" Color="#FFABAdB3"/>
        <SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA"/>
        <SolidColorBrush x:Key="TextBox.Focus.Border" Color="#FF569DE5"/>
        <Style x:Key="IconTextBoxStyle" TargetType="{x:Type TextBox}">
            <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Setter Property="AllowDrop" Value="true"/>
            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True" CornerRadius="8">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="2*"/>
                                    <ColumnDefinition Width="8*"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock FontFamily="/Assets/Fonts/#iconfont" Text="{TemplateBinding Tag}" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
                                <ScrollViewer Grid.Column="1" x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" VerticalAlignment="Center"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.MouseOver.Border}"/>
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
                        <Condition Property="IsSelectionActive" Value="false"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                </MultiTrigger>
            </Style.Triggers>
        </Style>        
    </Window.Resources>
    <Border Margin="5" Background="AntiqueWhite" CornerRadius="8">
        <Border.Effect>
            <DropShadowEffect BlurRadius="5"
                              Direction="0"
                              Opacity="0.3"
                              ShadowDepth="0"
                              Color="Gray" />
        </Border.Effect>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="6*" />
                <ColumnDefinition Width="4*" />
            </Grid.ColumnDefinitions>
            <Border CornerRadius="8,0,0,8">
                <Border.Background>
                    <ImageBrush ImageSource="./Assets/Images/LoginBackground.png" Stretch="Fill" />
                </Border.Background>
            </Border>
            <Border Grid.Column="1" CornerRadius="0,8,0,8">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="3*" />
                        <RowDefinition Height="7*" />
                    </Grid.RowDefinitions>
                    <StackPanel>
                        <Button Width="30" Height="30" HorizontalAlignment="Right" Template="{StaticResource ResourceKey=CloseButton}" />
                        <TextBlock Margin="15"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"
                                   FontSize="26"
                                   Foreground="Black"
                                   Text="图书管理系统" />
                        <TextBlock Margin="5"
                                   HorizontalAlignment="Center"
                                   FontSize="16"
                                   Text="MVVM示例Demo" />
                    </StackPanel>

                    <Grid Grid.Row="1" Margin="20,20">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="3*" />
                            <RowDefinition Height="3*" />
                            <RowDefinition Height="3*" />
                            <RowDefinition Height="1*" />
                        </Grid.RowDefinitions>
                        <TextBox Style="{DynamicResource IconTextBoxStyle}" Tag="&#xe6a4;" Height="60" FontSize="15"/>
                        <TextBox Grid.Row="1" Style="{DynamicResource IconTextBoxStyle}" Tag="&#xe603;" Height="60" FontSize="15" />
                        <Button Grid.Row="2" Width="200" Height="50" Template="{StaticResource LoginButton}" Background="Blue" Foreground="White" FontSize="20"/>
                    </Grid>
                </Grid>
            </Border>
        </Grid>
    </Border>
</Window>

这是登录页面,登录成功后的页面随便写个吧:handyControlView,尝鲜一下HandyControl

先安装HandyControl:

WPF 入门笔记 - 07 - MVVM示例

App.xaml中添加HandyControl资源字典:

<Application.Resources>
    <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
                <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

引入命名空间: xmlns:hc="https://handyorg.github.io/handycontrol",之后使用引用对应样式就可以了:

WPF 入门笔记 - 07 - MVVM示例

HandyControl分原生控件和拓展控件,原生控件是在WPF原生控件的基础上做了美化,本质功能是一样的,拓展控件则是原生控件中没有的,比如可以播放Gif动图的GifImage,我就看看原生控件了,扩展控件等专门用的时候再研究:

<Window x:Class="MVVMDemo.handyControlView"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       xmlns:controls="https://handyorg.github.io/handycontrol"
        xmlns:local="clr-namespace:MVVMDemo"
        mc:Ignorable="d"
        Title="handyControlView" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <Label Content="LabelDanger样式" Margin="10" Style="{DynamicResource LabelDanger}"></Label>

            <Button Content="Button" Style="{StaticResource ButtonPrimary}"/>
            <Button Content="这是一个按钮" Margin="10,0,0,0" controls:BorderElement.CornerRadius="15"/>
            <CheckBox Content="CheckBox" IsChecked="True"/>
            <CheckBox Margin="0,16,0,0" Content="CheckBox" IsChecked="True" IsEnabled="False"/>
            <CheckBox Margin="0,16,0,0" Content="CheckBox" IsChecked="{x:Null}"/>
            <Slider VerticalAlignment="Center" Width="400" Value="40" Maximum="100"/>
            <TextBox Width="200" VerticalAlignment="Center"/>
            <RadioButton Content="默认样式"/>
            <RadioButton Margin="0,16,0,0" Content="不可编辑" IsChecked="True" IsEnabled="False"/>
            <StackPanel Margin="20">
                <TextBlock Text="ProgressBarSuccess"></TextBlock>
                <ProgressBar Style="{DynamicResource ProgressBarSuccess}" Value="40"></ProgressBar>
            </StackPanel>
            <StackPanel Margin="20">
                <TextBlock Text="ProgressBarInfo"></TextBlock>
                <ProgressBar Style="{DynamicResource ProgressBarInfo}" Value="40"></ProgressBar>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

WPF 入门笔记 - 07 - MVVM示例

页面逻辑

不使用MVVM

在不使用MVVM情况下实现点击登录:点击登录按钮获取输入的用户名密码,验证是否正确,正确则创建页面隐藏当前页:

WPF 入门笔记 - 07 - MVVM示例

private void btnLogiin_Click(object sender, RoutedEventArgs e)
{
    if (txtUserName.Text == "Cass" && txtPassword.Text == "12136")
    {
        handyControlView handyControlView = new handyControlView();
        handyControlView.Show();
        this.Close();
    }
    else
    {
        MessageBox.Show("用户名或密码错误。");
    }
}

private void btnClose_Click(object sender, RoutedEventArgs e)
{
    this.Close();
}

WPF 入门笔记 - 07 - MVVM示例

现在所有东西是耦合在一起的,虽然这样看起来实现起来比较快,但是在后期维护起来是非常麻烦的,尤其是💩⛰️工程,假如其中任何一个环节发生了变化,程序就会出问题,简而言之就是牵一发而动全身。

接下来看一下MVVM模式下的实现:

WPF 入门笔记 - 07 - MVVM示例

Model部分

这部分主要是登录页面的数据部分,涉及两个字段,用户名和密码:

LoginModel.cs

namespace MVVMDemo
{
    public class LoginModel
    {
        private string _UserName;

        public string UserName
        {
            get { return _UserName; }
            set { _UserName = value; }
        }

        private string _Password;

        public string Password
        {
            get { return _Password; }
            set { _Password = value; }
        }
    }
}
View部分

视图部分主要做数据的呈现,通过绑定和命令解耦图形界面和数据以及执行动作之间的关系,数据之间的交互在用户名和密码文本框:

WPF 入门笔记 - 07 - MVVM示例

动作有两个,一个是关闭按钮,另一个是登录按钮。关闭按钮只是关闭当前窗体,不涉及业务交互,所以保留之前的Click事件即可,登录按钮就涉及到视图、数据以及业务逻辑了,这就需要通过命令来完成。

ViewModel部分

在这之前需要完成命令接口的定义,新建一个文件:RelayCommand.cs实现ICommand接口:

using System;
using System.Windows.Input;

namespace MVVMDemo
{
    public class RelayCommand : ICommand
    {

        private readonly Action _execute;   // 需要执行的操作(命令体)
        private readonly Func<bool> _canExecute;    // 命令是否可以执行的逻辑

        public RelayCommand(Action action, Func<bool> canExecute)
        {
            _execute = action;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)    // ICommand接口方法之一,用于判断命令是否可以执行
        {
            if (_canExecute == null)
            {
                return true;	// 命令始终可以执行
            }
            else
            {
                return _canExecute();	// 调用 _canExecute() 获取判断结果
            }
        }

        public void Execute(object parameter)   // ICommand接口方法之一 用于执行命令体,调用 _execute 所存储的操作
        {
            _execute?.Invoke();
        }

        public event EventHandler CanExecuteChanged	// ICommad接口中的事件,当命令的可执行状态发生变化时,触发此事件来通知界面元素更新
        {
            add
            {
                if (_canExecute != null) { CommandManager.RequerySuggested += value; }
            }
            remove
            {
                if (_canExecute != null) { CommandManager.RequerySuggested -= value; }
            }
        }
    }
}

这样就完成了命令接口的实现,接着来看ViewModel视图模型,视图模型作为沟通数据Model和视图View之间的桥梁,就得多干一些活了。首先是数据部分,为了更好的解耦,不让图形界面与数据产生直接交互,在ViewModel里嵌套一层在获取Model以及View部分的数据并完成验证处理、数据更新还有命令操作:

LoginViewModel.cs:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;

namespace MVVMDemo
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;	// INotifyPropertyChanged接口事件,用于在属性更改时发出通知

        private LoginModel _loginModel;	
        private MainView _mainView;

        public LoginViewModel(MainView mainView)
        {
            _loginModel = new LoginModel();
            _mainView = mainView;
        }

        // 绑定到登录界面文本框的属性,用于获取和设置用户名和密码 
        public string UserName	
        {
            get { return _loginModel.UserName; }
            set
            {
                _loginModel.UserName = value;
                OnPropertyChanged(UserName);
            }
        }

        public string Password
        {
            get { return _loginModel.Password; }
            set
            {
                _loginModel.Password = value;
                OnPropertyChanged(Password);
            }
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)	// 触发属性更改通知的方法
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private void LoginFunc()	// 处理登录操作
        {
            if (UserName == "Cass" && Password == "12136")
            {
                handyControlView handyControlView = new handyControlView();
                handyControlView.Show();
                _mainView.Close();
            }
            else
            {
                MessageBox.Show("用户名或密码错误");
                UserName = "";
                Password = "";
            }
        }

        private bool CanLoginExecute()
        {
            return true;
        }

        public ICommand LoginAction	// 绑定到登录按钮的命令属性
        {
            get
            {
                return new RelayCommand(LoginFunc, CanLoginExecute);	//(执行体,判断条件)
            }
        }
    }
}

完成了沟通部分的桥梁怎样,和其他两部分取得练习呢?数据部分,通过属性更新已经通知到了,视图部分就需要通过绑定数据上下文来完成了:

Mainview.xaml.cs

using System.Windows;

namespace MVVMDemo
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainView : Window
    {
        public MainView()
        {
            InitializeComponent();
            this.DataContext = new LoginViewModel(this);
        }

        private void btnClose_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }
}

🎉完结撒花

以上一个简单的MVVM示例就完成了,怎么说呢,虽然看着很简单,代码也没多少,但是对初学者来说绝非易事。还有一件事想要告诉大家,学习过程中一定不要只看不写,在学习过程中,实践是非常重要的。阅读和理解代码的同时,动手编写代码是巩固知识的关键部分。即使在完成这篇博文的过程中,我仍有新的收获在里面,所以大家千万不要只看,纸上谈兵永远都是空谈😉。

希望可以给大家一点参考,有错误的部分还请在评论区指出。文章来源地址https://www.toymoban.com/news/detail-645127.html

到了这里,关于WPF 入门笔记 - 07 - MVVM示例的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • WPF真入门教程23--MVVM简单介绍

            在WPF开发中,经典的编程模式是MVVM,是为WPF量身定做的模式,该模式充分利用了WPF的数据绑定机制,最大限度地降低了Xmal文件和CS文件的耦合度,也就是UI显示和逻辑代码的耦合度,如需要更换界面时,逻辑代码修改很少,甚至不用修改。与WinForm开发相比,我们一般

    2024年02月03日
    浏览(42)
  • WPF入门到跪下 第九章 MVVM-行为处理

    命令是指特定指令、有明确的执行内容且具有一定的强制性。 命令VS事件 命令与控件的事件看起来有类似的地方,但是两者是不同的。 控件的事件触发函数是在对应窗体的后台代码中进行定义的,仔细查阅窗体的后台代码,能发现这是一个部分类,也就是编译过后后台代码

    2024年02月02日
    浏览(40)
  • WPF入门到跪下 第九章 MVVM-跨模块交互

    在实际开发过程中,经常会遇到多个窗口对象,随之而来的就是对应的多个ViewModel对象,这些对象在一定条件下会发生相互访问的情况,例如VM与不同窗口交互、VM与不同VM交互,这些不同模块对象之间的交互,就是跨模块交互。 MVVM模式下跨模块交互解决方案 面对跨模块交互

    2024年02月02日
    浏览(35)
  • WPF入门到跪下 第九章 MVVM-基本数据处理

    MVVM是Model-View-ViewModel的缩写。mvvm是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI展现出来,ViewModel是一个同步View和Model的对象。 在MVVM架构下,View和Model之间没有直接的联系,它们通过Vie

    2024年01月21日
    浏览(48)
  • WPF入门到精通:3.MVVM简单应用及全局异常处理

    在WPF应用程序开发中,MVVM(Model-View-ViewModel)是一种非常流行的架构模式。它为应用程序的设计提供了良好的分层结构和可扩展性。 结构分为下列三部分 Model:定义了应用程序的数据模型 就是系统中的对象,可包含属性和行为(是一个class实体,是对现实中事物的抽象,开

    2024年02月11日
    浏览(36)
  • WPF入门教程系列二十五——DataGrid使用示例(2)

    WPF入门教程系列目录 WPF入门教程系列二——Application介绍 WPF入门教程系列三——Application介绍(续) WPF入门教程系列四——Dispatcher介绍 WPF入门教程系列五——Window 介绍 WPF入门教程系列十一——依赖属性(一) WPF入门教程系列十五——WPF中的数据绑定(一)        默认情况下

    2024年02月05日
    浏览(49)
  • WPF入门教程系列二十六——DataGrid使用示例(3)

    WPF入门教程系列目录 WPF入门教程系列二——Application介绍 WPF入门教程系列三——Application介绍(续) WPF入门教程系列四——Dispatcher介绍 WPF入门教程系列五——Window 介绍 WPF入门教程系列十一——依赖属性(一) WPF入门教程系列十五——WPF中的数据绑定(一)   五、DataGrid的DataG

    2024年02月06日
    浏览(41)
  • WPF入门教程系列二十四——DataGrid使用示例(1)

    WPF入门教程系列目录 WPF入门教程系列二——Application介绍 WPF入门教程系列三——Application介绍(续) WPF入门教程系列四——Dispatcher介绍 WPF入门教程系列五——Window 介绍 WPF入门教程系列十一——依赖属性(一) WPF入门教程系列十五——WPF中的数据绑定(一)           WPF技术

    2024年02月04日
    浏览(37)
  • 《Unity Shader 入门精要》笔记07

    Unity中通常使用两种方法来实现透明效果:第一种是试用 透明度测试(Alpha Test) ,这种方法其实无法得到真正的半透明效果;另一种是 透明度混合(Alpha Blending) 。 由于深度缓冲的存在,可以让不透明物体不考虑他们渲染顺序也能得到正确的排序效果。但是实现透明效果需

    2024年02月07日
    浏览(43)
  • WPF CommunityToolkit.Mvvm

    CommunityToolkit.Mvvm(以下简称Toolkit)是WPF最有名的两个框架,一个是Prism,另一个就是Toolkit。 Prism可以看我的Prism详解 WPF Prims框架详解 Toolkit 官方文档 用 CommunityToolkit.Mvvm 加速 MVVM 开发流程 Toolkit简单复写了我们常用的两个方法 一个是 SetProperty,一个是RelayCommand SetProperty,通知

    2024年02月12日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包