首页 > 其他分享 >《深入浅出WPF》MVVM视频教材中的实例练习

《深入浅出WPF》MVVM视频教材中的实例练习

时间:2022-08-21 11:47:05浏览次数:76  
标签:ViewModel 界面 string MVVM get 深入浅出 使用 WPF public

前言

在学习了《深入浅出WPF》的书籍以及视频之后,将最后的MVVM练习项目从头到尾敲了一遍,以加深自己的理解,也是为了提高自己对基础知识的熟练程度。由于是自己对着示例图自己敲的,所以代码可能和视频中的不太一致,但是思路是差不多的。

思路剖析

1. 首先看看需要完成的图形界面,了解自己需要完成的功能进行分解

  • 左上角有一个标题
  • 主界面中
    • 最外层有一个边框
    • 中间又看到一个关于店铺的信息
    • 接着往下是一个关于店铺出售的菜品
    • 最后是关于计数框和一个下单按钮

2. 构思界面大概需要用什么元素来布局

这个视频中的MVVM虽然用到了Prism,但是只是使用了最基础和最核心的数据绑定和命令的功能,并没有使用关于组件化的功能,所以我就没有按照视频那样使用Prism,而是使用微软提供的 CommunityToolkit.Mvvm

然后是关于布局,最外层元素应该使用一个 Border 元素,然后内部使用 Grid 元素切分为三行,第一行是关于店铺的信息,可以使用 Grid(个人编写时使用了) 继续切分为三行,或者 StackPanel(视频中是使用了),相对来说,StackPanel 可能更加简单点;然后是第二行,应该使用 DataGrid 来排版;第三行则是一个 StackPanel ,然后水平放置

大概布局就这样策划,就可以实现界面的效果了

3. 构思界面中的数据如何建模

对于界面中的数据,如果考虑复用性,也就是这个界面可能会在其他地方使用,那么关于店铺的信息,应该都是动态的,也就是我们应该有一个 店铺 的模型,用来装相关的信息,比如店铺的名字,地址,联系电话等;

然后是中间的菜单信息,它可以和店铺信息直接关联起来,也可以单独出来一个模型,个人觉得,拆开会更加灵活,因为我认为菜品并属于店铺的,可以说店铺有这些菜,但不是这些菜属于这些店铺,它应该是单独出来的一个模型,而且如果是在组件化的前提下,中间这个列表作为一个组件,使用一个ViewModel来对应这个菜单会更加自然

然后还可以看到,列表中有一个选中的列,这个列是不存在菜单中,它是一个功能性属性,它只存在于ViewModel中,但是不在Model中

然后最后有一个计数的文本框,这个框中的数据会根据用户选中的菜来计数,也就是说,在用户选中菜之后,会有一个命令产生,然后根据命令,会计算当前选中的菜的个数,然后将这个数据更新,然后推动页面的更新,所以会有一个计数数据存在,一个命令在 checkbox

最后是按钮,这里会有一个命令产生,这个命令会将选中的菜都记录下来

4. 实现

视频中是按照Service、Model、ViewModel、View的顺序实现的,也就是自下而上的顺序,而我是View、Model、Service、ViewModel的顺序来实现的。

首先这个是练手项目,然后没人设计界面然后可以直接使用,所以我需要首先布局,这样我可以得到一个没有数据的空界面或者假数据的界面,这样可以直观的看到界面的功能,然后其实除了界面之外,我的步骤依然是自下而上的,先建模,再组合,个人认为ViewModel的作用之一就是这样,我们的建模与实际界面展示的数据是有差距的,而ViewModle就可以屏蔽这部分差距。

4.1 页面实现

<Border BorderBrush="Orange" BorderThickness="3" Background="Yellow" CornerRadius="6">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Border BorderBrush="Orange" BorderThickness="1" Margin="4" Padding="4" CornerRadius="6">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <StackPanel Orientation="Horizontal" Grid.Row="0">
                        <StackPanel.Effect>
                            <DropShadowEffect Color="LightGray" />
                        </StackPanel.Effect>
                        <TextBlock Text="欢迎光临 - " FontSize="60"/>
                        <TextBlock Text="香喷喷鸡排店" FontSize="60"/>
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Grid.Row="1">
                        <TextBlock Text="店铺地址:" FontSize="24" />
                        <TextBlock Text="XXXXXXXXXXXX" FontSize="24" />
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Grid.Row="2">
                        <TextBlock Text="订餐电话:" FontSize="24" />
                        <TextBlock Text="XXXXXXXXXXXXX" FontSize="24" />
                    </StackPanel>
                </Grid>

            </Border>

            <DataGrid Grid.Row="1" AutoGenerateColumns="False" GridLinesVisibility="None" 
                      CanUserAddRows="False" CanUserDeleteRows="False" Margin="4 0" FontSize="16"
                      >
                <DataGrid.Columns>
                    <DataGridTextColumn Header="菜品" Width="120" />
                    <DataGridTextColumn Header="种类" Width="120" />
                    <DataGridTextColumn Header="点评" Width="120" />
                    <DataGridTextColumn Header="推荐分数" Width="120" />
                    <DataGridTextColumn Header="选中" Width="120" />
                </DataGrid.Columns>
            </DataGrid>

            <StackPanel Grid.Row="2" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0 2">
                <TextBlock Text="共计:" VerticalAlignment="Center"/>
                <TextBox IsReadOnly="True" 
                         Text="0"
                         Padding="3"
                         TextAlignment="Center" Width="120" Margin="4 0"/>
                <Button Content="下单" Width="120" Height="24"/>
            </StackPanel>
        </Grid>
    </Border>

按照上面的思路,首先用VS实现一个界面,然后填上一些假数据,看看界面的外观是否符合要求

4.2 模型实现

然后是建模,如果是正式的项目中,我们的数据来源可能是数据库或者网络,现在的练手项目中使用的一个xml文件来存储数据,主要还是关于菜单的数据,店铺的信息是写死的

  • 文件数据格式
<?xml version="1.0" encoding="utf-8"?>
<Dishes>
    <Dish>
        <Name>红烧牛肉</Name>
        <Category>烧菜</Category>
        <Comment>本店特色</Comment>
        <Score>4.5</Score>
    </Dish>
    <Dish>
        <Name>红烧牛肉</Name>
        <Category>烧菜</Category>
        <Comment>本店特色</Comment>
        <Score>4.5</Score>
    </Dish>
</Dishes>
  • 菜单模型类
public class Dish
{
    public string Name { get; set; }
    public string Category { get; set; }
    public string Comment { get; set; }
    public double Score { get; set; }

    public Dish(string name, string category, string comment, double score)
    {
        Name = name;
        Category = category;
        Comment = comment;
        Score = score;
    }
}

因为我使用的是.Net6版本,所以会有一个问题是,默认情况下属性类型都是不可空的,所以就需要创建一个构造函数,正常应该是评论和分数是可空的,这里就不管了

  • 店铺模型类
public class Restaurant
{
    public string Name { get; set; }
    public string Adress { get; set; }
    public string PhoneNumber { get; set; }

    public Restaurant(string name, string adress, string phoneNumber)
    {
        Name = name;
        Adress = adress;
        PhoneNumber = phoneNumber;
    }
}

4.3 服务实现

有了模型,也就是有了数据,那么数据的访问就需要通过服务来进行,而不是直接访问。正常情况下,我们应该建立一个核心层,创建关于数据访问的接口,方便以后的迁移,现在是文件形式,以后可能是数据库或者网络请求,多种多样,所以在建立了接口之后,再通过IoC和DI,尽可能隔离数据访问(基础服务)和ViewModel也即是业务层的关联,这样系统可以在业务不变的情况下,分别进化和切换

  • 数据访问服务
public interface IDataService
{
    List<Dish> GetAllDishes();
}
public class XmlDataService : IDataService
{
    public List<Dish> GetAllDishes()
    {
        var filePath = Path.Combine(Environment.CurrentDirectory, @"Data/Data.xml");

        var document = XDocument.Load(filePath);

        var dishes = document.Descendants("Dish");

        return dishes
            .Select(element =>
                new Dish(element.Element("Name")!.Value,
                    element.Element("Category")!.Value,
                    element.Element("Comment")!.Value,
                    double.Parse(element.Element("Score")!.Value)))
            .ToList();
    }
}
  • 订单服务
public interface IOrderService
{
    void PlaceOrder(List<string> dishes);
}
public class MockOrderService : IOrderService
{
    public void PlaceOrder(List<string> dishes)
    {
        File.WriteAllLines(@"Data/order.txt", dishes.ToArray());
    }
}

同理,订单服务也是如此,建立接口层,因为以后的订单是如何实现的,现在还不清楚,可以推迟实现,暂时是写入文件,以后可能是交给别的系统处理,可能异步发送信息到消息中间件等等

4.4 业务实现

最后,最核心也是最复杂的业务部分的实现,交给了ViewModel,前文提到,我使用的MVVM的工具是官方的 CommunityToolkit.Mvvm ,然后使用了 ObservableObject 实现ViewModel的数据更新时,能推动界面的变更,然是是使用了 RelayCommand 实现命令

4.4.1 菜单业务

public class DishItemViewModel : ObservableObject
{
    public Dish Dish { get; }

    private bool _isSelected;
    
    public bool IsSelected
    {
        get => _isSelected;
        set => SetProperty(ref _isSelected, value);
    }

    public DishItemViewModel(bool isSelected, Dish dish)
    {
        _isSelected = isSelected;
        Dish = dish;
    }
}

因为菜品是不会交给用户增删的,所以数据也就不会变更,不需要使用 SetProperty 方法。

具体的MVVM的使用方法,可以参阅官方文档,直接搜索MVVM关键字即可,我也是直接看的文档,还算简单,有兴趣的可以看看,这个工具包在实现小工具或小项目的时候,还是很轻量实用的

顺提一下, MvvmLight 这个框架在网上或者视频教程中经常出现,属于以前的轻量级MVVM框架,但是它好像在18年就停止维护了,而且不支持我现在的.Net6

4.4.2 主窗体业务

public class MainWindowViewModel : ObservableObject
{

    public ICommand PlaceOderCommand { get; }
    public ICommand SelectMenuItemCommand { get; }
    
    private int _count;

    public int Count
    {
        get => _count;
        set => SetProperty(ref _count, value);
    }

    private Restaurant _restaurant;

    public Restaurant Restaurant
    {
        get => _restaurant;
        set => SetProperty(ref _restaurant, value);
    }

    private List<DishItemViewModel> _dishMenu;

    public List<DishItemViewModel> DishMenu
    {
        get => _dishMenu;
        set => SetProperty(ref _dishMenu, value);
    }

    public MainWindowViewModel()
    {
        LoadRestaurant();
        LoadDishMenu();
        PlaceOderCommand = new RelayCommand(PlaceOderCommandExecute);
        SelectMenuItemCommand = new RelayCommand(SelectMenuItemCommandExecute);
    }

    private void LoadRestaurant()
    {
        Restaurant = new Restaurant("香喷喷鸡排店", "广东省XX市XX区XX镇", "123456789321");
    }

    private void LoadDishMenu()
    {
        // TODO 此处应该使用DI
        var xmlDataService = new XmlDataService();
        DishMenu = xmlDataService.GetAllDishes()
            .Select(d => new DishItemViewModel(false, d))
            .ToList();
    }

    private void PlaceOderCommandExecute()
    {
        // TODO 此处应该使用DI
        var list = DishMenu.Where(d => d.IsSelected).Select(d => d.Dish.Name).ToList();
        var orderService = new MockOrderService();
        orderService.PlaceOrder(list);
        MessageBox.Show("订餐成功");
    }

    private void SelectMenuItemCommandExecute()
    {
        Count = DishMenu.Count(d => d.IsSelected);
    }
}

可以看到,主界面中,ViewModel组合了店铺信息、菜单信息和计数,还包含了两个命令,会显得多少有些拥挤,如果是组件化开发,这里大概会切分为三四个组件,然后每个组件有自己的ViewModel,这样就显得更加整洁,分门别类,有利于维护

然后在使用服务的地方都是直接构造实例使用,如果是正常情况下,会使用DI注入服务,再使用

4.5 对接UI

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}
<Window x:Class="CrazyElephant.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CrazyElephant" 
        xmlns:viewmodels="clr-namespace:CrazyElephant.ViewModels"
        d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"
        mc:Ignorable="d"
        Title="{Binding Restaurant.Name,StringFormat=\{0\}-在线订餐}" Height="600" Width="800">
    <Border BorderBrush="Orange" BorderThickness="3" Background="Yellow" CornerRadius="6">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Border BorderBrush="Orange" BorderThickness="1" Margin="4" Padding="4" CornerRadius="6">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <StackPanel Orientation="Horizontal" Grid.Row="0">
                        <StackPanel.Effect>
                            <DropShadowEffect Color="LightGray" />
                        </StackPanel.Effect>
                        <TextBlock Text="欢迎光临 - " FontSize="60"/>
                        <TextBlock Text="{Binding Restaurant.Name}" FontSize="60"/>
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Grid.Row="1">
                        <TextBlock Text="店铺地址:" FontSize="24" />
                        <TextBlock Text="{Binding Restaurant.Adress}" FontSize="24" />
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Grid.Row="2">
                        <TextBlock Text="订餐电话:" FontSize="24" />
                        <TextBlock Text="{Binding Restaurant.PhoneNumber}" FontSize="24" />
                    </StackPanel>
                </Grid>

            </Border>

            <DataGrid Grid.Row="1" AutoGenerateColumns="False" GridLinesVisibility="None" 
                      CanUserAddRows="False" CanUserDeleteRows="False" Margin="4 0" FontSize="16"
                      ItemsSource="{Binding DishMenu}">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="菜品" Width="120" Binding="{Binding Dish.Name}"/>
                    <DataGridTextColumn Header="种类" Width="120" Binding="{Binding Dish.Category}"/>
                    <DataGridTextColumn Header="点评" Width="120" Binding="{Binding Dish.Comment}"/>
                    <DataGridTextColumn Header="推荐分数" Width="120" Binding="{Binding Dish.Score}"/>
                    <DataGridTemplateColumn Header="选中" Width="120" SortMemberPath="IsSelected">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
                                          VerticalAlignment="Center" HorizontalAlignment="Center"
                                          Command="{Binding Path=DataContext.SelectMenuItemCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGrid}}}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>

            <StackPanel Grid.Row="2" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0 2">
                <TextBlock Text="共计:" VerticalAlignment="Center"/>
                <TextBox IsReadOnly="True" 
                         Text="{Binding Count}"
                         Padding="3"
                         TextAlignment="Center" Width="120" Margin="4 0"/>
                <Button Content="下单" Width="120" Height="24" 
                        Command="{Binding PlaceOderCommand}"/>
            </StackPanel>
        </Grid>
    </Border>
</Window>

首先是在主窗体中关联ViewModel,然后在xmal中添加(VS提示我加上的)

xmlns:viewmodels="clr-namespace:CrazyElephant.ViewModels"
d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"

这样的效果是我们在xmal中编写 Binding 会有提示,这样就不怕写错了

然后就是基础中的数据绑定以及命令绑定

5. 总结

该项目主要目的是为了熟悉MVVM最核心和最基础的功能,也就是数据绑定和命令的使用。

在MVVM模式下,最明显的好处是,我们的界面和业务是彻底分开的,我们的业务中没有出现与界面有关的任何变量(如何是事件驱动的模式,业务中会出现很多使用界面控件key来填充数据的动作),由于与界面没有关联,所以在业务不变的情况下,界面是可以自由变换的,同样是菜单,我可以不使用 DataGrid 实现,可以修改为其他控件,但是我们的业务代码一行都不需要修改

更进一步,我们看到,MVVM模式的重点是在View-ViewModel中,而我们的Model和Service是相对要更普通,但是这有一个好处就是,这部分的实现和Web开发中的对应部分是相差无几的,或者说一模一样的,那么在基础服务一致,业务和界面分离的情况下,完全可以将界面换成网页的前端框架来实现,然后在业务层上添加一层WebApi层,就可以实现界面的切换或者多端实现应该也是可行的

而且在项目中,会发现我们需要组件化开发,需要IoC,需要DI,这样我们的开发会更加简单快捷,这些都可以东拼西凑实现,也可以使用Prism来大包圆

在组件化之后,需要考虑的问题是,组件之间的通信,如果传递数据,这里涉及一个消息总线或者消息队列的实现,后期使用到Prism的时候,应该会学习到

然后是界面部分,因为之前学习过网页端,所以在对比了两者之后,在布局方面,我更喜欢WPF,它的布局更加容易上手,容易设计,目前还没有出现奇奇怪怪的布局失败的情况,但是网页端的基本没成功过,我认为WPF的唯一缺点是只能在Windows平台运行

标签:ViewModel,界面,string,MVVM,get,深入浅出,使用,WPF,public
From: https://www.cnblogs.com/huangwenhao1024/p/16609685.html

相关文章

  • WPF中向下拉框中绑定枚举体
    1、枚举绑定combox的ItemsSourceItemsSource绑定的是个集合值,要想枚举绑定ItemsSource,首先应该想到的是把枚举值变成集合。方法一:使用资源里的ObjectDataProvider如以下枚......
  • 浅谈MVVM开发思想
    IT流行语:程序=算法+数据结构。还有一句话,程序=输入数据->数据处理->输出数据。如果以编程语言理解这句话,算法是方法,数据结构就是变量的组织形式,那么这句话可以理解......
  • WPFGroupBox控件自定义
    先上效果图  直接上代码(直接在Window.Resources里面添加这段代码)<StyleTargetType="GroupBox"><SetterProperty="Margin"Value="10,5"/>......
  • WPF将控件转为图片(Visual试图转Bitmap)
    RenderTargetBitmaptarget=newRenderTargetBitmap((int)grid.ActualWidth,(int)grid.ActualHeight,96d,96d,PixelFormats.Default);target.Render(grid);Cropped......
  • 【WPF】命令系统
    命令四要素1、命令,一般情况都是使用”路由ui命令“2、命令源:触发命令的地方。3、命令绑定:将命令和执行方法绑定,然后在将commandbing放置在,命令目标的外围ui控件上,这样......
  • DW组队学习——深入浅出PyTorch笔记
    本篇是针对DataWhale组队学习项目——深入浅出PyTorch而整理的学习笔记。由于水平实在有限,不免产生谬误,欢迎读者多多批评指正。安装PyTorch安装Anaconda这里为了避免手......
  • C# WPF 访问剪切板报错
    如果剪贴板操作失败(例如 HRESULT0x800401D0(CLIPBRD_E_CANT_OPEN) 错误),则会引发相应的 ExternalException (,这是一种ExternalException)。由于Win32 OpenClipbo......
  • C#-WPF-LiveChart大数据时图标绘制(曲线图)并支持图片保存
    xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"<Button   Name="SaveBtn"   Grid.Row="0"   Width="100"   Height="32"   ......
  • 【WPF】XDG0062 必须使“Setter.Property”具有非 null 值。
    编译环境vs2022.net6.0在样式中给附加属性设置触发条件。显示XDG0062错误,但是代码能正常编译和运行。   编辑环境中运行后,能正常运行   解决方法......
  • XAML、WPF 学习笔记
    容器学习:所有的WPF布局容器都派生自System.Windows.Controls.Panel。Panel继承自FrameworkElement。在Panel中有一个比较重要的属性是UIElementCollection类型的Chi......