WPF13-MVVM进阶

目录

    • [1. 窗体设置](#1. 窗体设置)
    • [2. 字体图标](#2. 字体图标)
    • [3. 控件模板](#3. 控件模板)
    • [4. 页面逻辑](#4. 页面逻辑)
      • [4.1. 不使用MVVM](#4.1. 不使用MVVM)
      • [4.2. MVVM模式实现](#4.2. MVVM模式实现)

本篇我们开发一个基于MVVM的登录页面,用来回顾下之前学习的内容

登录页面如下:

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

1. 窗体设置

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

xml 复制代码
<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>

2. 字体图标

去阿里巴巴矢量图标库选好要用的图标,添加入库,然后下载下来添加至项目:

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

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

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

3. 控件模板

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

xml 复制代码
<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>

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

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

xml 复制代码
<Window x:Class="MVVMDemo2.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:MVVMDemo2"
        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="&#xe63c;" Height="60" FontSize="15"/>
                        <TextBox Grid.Row="1" Style="{DynamicResource IconTextBoxStyle}" Tag="&#xe675;" 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>

4. 页面逻辑

4.1. 不使用MVVM

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

后台Login.xaml.cs:

csharp 复制代码
 private void btnClose_Click(object sender, RoutedEventArgs e)
 {
     this.Close();
 }


 private void btnLogin_Click(object sender, RoutedEventArgs e)
 {
     if (txtUserName.Text == "admin" && txtPassword.Text == "123456")
     {
         MainWindow main = new MainWindow();
         main.Show();
         this.Close();
     }
     else
     {
         MessageBox.Show("用户名或密码错误。");
     }
 }

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

4.2. MVVM模式实现

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

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

LoginModel.cs:

csharp 复制代码
 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; }
     }
 }
  1. View部分
    视图部分主要做数据的呈现,通过绑定和命令解耦图形界面和数据以及执行动作之间的关系,数据之间的交互在用户名和密码文本框:

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

  1. ViewModel部分
    在这之前需要完成命令接口的定义,新建一个文件:RelayCommand.cs实现ICommand接口:
csharp 复制代码
    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:

csharp 复制代码
 public class LoginViewModel : INotifyPropertyChanged
 {
     public event PropertyChangedEventHandler PropertyChanged;
     private LoginModel _loginModel;
     private Login _loginView;


     public LoginViewModel(Login view)
     {
         _loginModel = new LoginModel();
         _loginView = view;
     }


     // 绑定到登录界面文本框的属性,用于获取和设置用户名和密码 
     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 == "admin" && Password == "123456")
         {
             MainWindow main = new MainWindow();
             main.Show();
             _loginView.Close();
         }
         else
         {
             MessageBox.Show("用户名或密码错误");
             UserName = "";
             Password = "";
         }
     }


     private bool CanLoginExecute()
     {
         return true;
     }


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

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

Login.xaml.cs:

csharp 复制代码
 public partial class Login : Window
 {
     public Login()
     {
         InitializeComponent();
         this.DataContext = new LoginViewModel(this);
     }


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


    
 }

以上一个简单的MVVM示例就完成了,怎么说呢,虽然看着很简单,代码也没多少,但是对初学者来说绝非易事。希望大家能多多练习,多多思考,多多总结,多多交流,多多进步。

详细代码我上传到gitee:https://gitee.com/zypapa100/MVVMPractice

相关推荐
苏克贝塔5 小时前
WPF12-MVVM
wpf
Vicky&James6 小时前
WPF到Web的无缝过渡:英雄联盟客户端项目OpenSilver迁移实战
前端·wpf
苏克贝塔6 小时前
WPF11-附加属性
wpf
吾与谁归in8 小时前
C#实现本地Deepseek模型及其他模型的对话
人工智能·c#·wpf·deepseek
lisenustc1 天前
WPF学习之Prism(二)
学习·wpf
百里与司空3 天前
WPF基本布局基础
学习·wpf
qq_382391334 天前
WPF框架学习
学习·wpf·1024程序员节
CE贝多芬4 天前
WPF的页面设计和实用功能实现
c#·wpf
酷炫码神4 天前
WPF布局控件
wpf