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

相关推荐
明耀2 小时前
WPF ListBox双击事件
wpf
wqq10273 小时前
WPF 依赖注入启动的问题
wpf
wqq10276 小时前
WPF 使用 DI EF CORE SQLITE
sqlite·wpf
Marzlam1 天前
一文读懂WPF系列之MVVM
wpf
Marzlam1 天前
一文读懂WPF系列之依赖属性与附加属性
wpf
zxb11c1 天前
WPF 中的元素继承层次结构 ,以下是对图中内容的详细说明:
wpf
Zhen (Evan) Wang1 天前
Margin和Padding在WPF和CSS中的不同
css·wpf
Marzlam2 天前
一文读懂WPF布局
wpf
WineMonk2 天前
.NET WPF 控件类分层结构
.net·wpf
Marzlam2 天前
一文读懂WPF系列之控件模版数据模板
wpf