前言
在学习了《深入浅出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