【Maui】视图界面与数据模型绑定
文章目录
- 前言
- 一、问题描述
- 二、解决方案
- 三、软件开发(源码)
- 3.1 创建模型
- 3.2 视图界面
- 3.3 控制器逻辑层
- 四、项目展示
前言
.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动和桌面应用。
使用 .NET MAUI,可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。
.NET MAUI 是一款开放源代码应用,是 Xamarin.Forms 的进化版,从移动场景扩展到了桌面场景,并从头重新生成了 UI 控件,以提高性能和可扩展性。 如果以前使用过 Xamarin.Forms 来生成跨平台用户界面,那么你会注意到它与 .NET MAUI 有许多相似之处。 但也有一些差异。 通过使用 .NET MAUI,可使用单个项目创建多平台应用,但如果有必要,可以添加特定于平台的源代码和资源。 .NET MAUI 的主要目的之一是使你能够在单个代码库中实现尽可能多的应用逻辑和 UI 布局。
一、问题描述
MVVM模式
(Model-View-ViewModel)架构模式,是将View和ViewModel关联起来,通过双向数据绑定实现View和ViewModel的同步更新。View负责展示数据和用户交互,ViewModel负责处理数据和业务逻辑,Model负责存储数据。MVVM的优点是能够降低View和ViewModel之间的耦合,使得代码更加可维护和可测试。
.NET MAUI是如何进行将View和ViewModel双向绑定的呢?
二、解决方案
1、视图–数据模型绑定:定义ViewModels
,视图层通过Binding
属性绑定ViewModels
;
2、数据模型–视图绑定:ViewModels
属性发生改变,需要通知View
进行更新,通知采用观察者模式
,更新View
采用委托Invoke
。
听起来很复杂对不对?其实很简单。
三、软件开发(源码)
3.1 创建模型
文件名:TitleBarViewModel.cs
位置:ViewModels
备注:集合一定要定义成 ObservableCollection,不要使用List,否则无法实现MVVM,ObservableCollection实现INotifyCollectionChanged, INotifyPropertyChanged。
using App.Mes.Core.Operation.Services.Mobile;
using Newtonsoft.Json;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace GlueNet.Mobile.ViewModels
{
public class TitleBarViewModel : INotifyPropertyChanged
{
private string _version;
private string _workdate;
private string _classesValue; //班组
private string _userID;
private string _userName;
private string _userComb; // 用户编号+姓名
public ObservableCollection<KeyValuePair<string, string>> ClassesOptions { get; set; } = new ObservableCollection<KeyValuePair<string, string>>();
public string ClassesValue
{
get => _classesValue;
set
{
if (_classesValue != value)
{
_classesValue = value;
OnPropertyChanged();
}
}
}
public string Version
{
get => _version;
set
{
_version = value;
OnPropertyChanged();
}
}
public string WorkDate
{
get => _workdate;
set
{
if (_workdate != value)
{
_workdate = value;
OnPropertyChanged();
}
}
}
public string UserID
{
get => _userID;
set
{
_userID = value;
OnPropertyChanged();
}
}
public string UserName
{
get => _userName;
set
{
_userName = value;
OnPropertyChanged();
}
}
public string UserComb
{
get => _userComb;
set
{
_userComb = value;
OnPropertyChanged();
}
}
/// <summary>
/// 构造函数
/// </summary>
public TitleBarViewModel()
{
InitializeOptions();
}
private void InitializeOptions()
{
//取班组
string str_Reason = GycMobileService.Proxy.GetKeyValue("class_group");
var ReasonList = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(str_Reason);
foreach (var item in ReasonList)
{
ClassesOptions.Add(new KeyValuePair<string, string>(item.Key, item.Value));
}
ClassesValue = ClassesOptions.FirstOrDefault().Value;
}
/// <summary>
/// 班组
/// </summary>
public string GetClassesValueByKey(string key)
{
var Classes = ClassesOptions.FirstOrDefault(x => x.Key == key);
return Classes.Value;
}
//实现了INotifyPropertyChanged接口,用于在属性值发生变化时通知界面更新。
public event PropertyChangedEventHandler PropertyChanged;
//事件委托更新属性
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
文件名:MO1002DetailsModel.cs
位置:ViewModels
备注:集合一定要定义成 ObservableCollection,不要使用List,否则无法实现MVVM,ObservableCollection实现INotifyCollectionChanged, INotifyPropertyChanged。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GlueNet.Mobile.Models
{
public class MO1002DetailsModel
{
/// <summary>
/// 预留申请单主号
/// </summary>
public virtual string CReservedNo { get; set; }
/// <summary>
/// 创建人
/// </summary>
public virtual string Creator { get; set; }
/// <summary>
/// 创建人
/// </summary>
public virtual string CreatorCn { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public virtual DateTime? CreateTime { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public virtual string CreateTimeText { get; set; }
/// <summary>
/// 单据类型(1 移库 2 领料 3退库)
/// </summary>
public virtual string CReservedType { get; set; }
/// <summary>
/// 单据类型(1 移库 2 领料 3退库)
/// </summary>
public virtual string CReservedTypeCn { get; set; }
/// <summary>
/// 物料类型 1-散料 2 纸卷(21大纸,22小纸,23纸垛,26tissue半成品) ( 暂不用mtrl_type)
/// </summary>
public virtual string CMtrlType { get; set; }
/// <summary>
/// 物料类型 1-散料 2 纸卷(21大纸,22小纸,23纸垛,26tissue半成品) ( 暂不用mtrl_type)
/// </summary>
public virtual string CMtrlTypeCn { get; set; }
/// <summary>
/// 发货实体库代码
/// </summary>
public virtual string CSendStoreHouse { get; set; }
/// <summary>
/// 发货实体库代码
/// </summary>
public virtual string CSendStoreHouseCn { get; set; }
/// <summary>
/// 发货业务工厂代码
/// </summary>
public virtual string CSendBnPlantId { get; set; }
/// <summary>
/// 发货业务工厂
/// </summary>
public virtual string CSendBnPlantIdCn { get; set; }
/// <summary>
/// 发货班次
/// </summary>
public virtual string CClassRate { get; set; }
/// <summary>
/// 备注
/// </summary>
public virtual string CRemark { get; set; }
/// <summary>
/// 备注
/// </summary>
public virtual string CRemarkCn { get; set; }
/// <summary>
/// 是否被选中
/// </summary>
public virtual bool IsCheck { get; set; } = false;
}
}
3.2 视图界面
文件名:MO1002Page.xaml
位置:Pages
备注:使用Mode=TwoWay,使用双向绑定,可以不设置观察者模式;
遍历中的RadioButton
不要使用CheckedChanged()
属性,亲测有bug,在2条数据被删除1条数据时,页面自然只剩1条数据,RadioButton
会自动被勾选,但是不会触发CheckedChanged()
,不建议使用CheckedChanged()
属性。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="GlueNet.Mobile.Pages.MO1002Page"
Title="件次退料">
<StackLayout>
<!--顶部标题栏-->
<Grid BackgroundColor="{StaticResource Gray300}" Padding="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 显示当前日期,左边部分 -->
<DatePicker x:Name="HiddenDatePicker"
Date="{Binding TitleBar.WorkDate}"
TextColor="White" Format="yyyy-MM-dd"
HorizontalOptions="Start"
VerticalOptions="Center"
Grid.Column="0" DateSelected="OnDateSelected" />
<!-- 显示班组,绑定点击事件 -->
<Label Text="{Binding TitleBar.ClassesValue}"
TextColor="White"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Column="1"
x:Name="ClassesLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnClassesClicked" />
</Label.GestureRecognizers>
</Label>
<!-- 显示登录用户名,绑定点击事件 -->
<Label Text="{Binding TitleBar.UserName}"
TextColor="White"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Column="2"
x:Name="UserNameLabel">
</Label>
</Grid>
<!--中部数据区域-->
<ScrollView VerticalOptions="FillAndExpand">
<CollectionView ItemsSource="{Binding DataList}" SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<HorizontalStackLayout>
<!--必须要,否则前端有bug-->
<RadioButton GroupName="DataListGroup" IsChecked="{Binding IsCheck, Mode=TwoWay}"/>
<Frame BorderColor="LightGray" CornerRadius="5" Padding="10" Margin="5">
<Grid ColumnDefinitions="*,*,*,*" RowDefinitions="*,*,*,*,*,*">
<Label Grid.Column="0" Text="退库单号:" FontSize="Small" />
<Label Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding CReservedNo}" FontSize="Small" />
<Label Grid.Row="1" Grid.Column="0" Text="单据类型:" FontSize="Small" />
<Label Grid.Row="1" Grid.Column="1" Text="{Binding CReservedTypeCn}" FontSize="Small"/>
<Label Grid.Row="1" Grid.Column="2" Text="物料类型:" FontSize="Small" />
<Label Grid.Row="1" Grid.Column="3" Text="{Binding CMtrlTypeCn}" FontSize="Small"/>
<Label Grid.Row="2" Grid.Column="0" Text="线边库:" FontSize="Small" />
<Label Grid.Row="2" Grid.Column="1" Text="{Binding CSendStoreHouseCn}" FontSize="Small"/>
<Label Grid.Row="3" Grid.Column="0" Text="业务工厂:" FontSize="Small" />
<Label Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding CSendBnPlantIdCn}" FontSize="Small"/>
<Label Grid.Row="4" Grid.Column="0" Text="创建人:" FontSize="Small" />
<Label Grid.Row="4" Grid.Column="1" Text="{Binding CreatorCn}" FontSize="Small"/>
<Label Grid.Row="4" Grid.Column="2" Text="创建时间:" FontSize="Small" />
<Label Grid.Row="4" Grid.Column="3" Text="{Binding CreateTimeText}" FontSize="Small"/>
<Label Grid.Row="5" Grid.Column="0" Text="退库原因:" FontSize="Small" />
<Label Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding CRemarkCn}" FontSize="Small" />
</Grid>
</Frame>
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ScrollView>
<!--底部操作栏-->
<Grid HeightRequest="60" Padding="5" BackgroundColor="{StaticResource Gray300}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button HorizontalOptions="Center" Text="刷新" FontSize="Small" BackgroundColor="LightBlue" Clicked="OnRefreshClicked"/>
<Button Grid.Column="1" HorizontalOptions="Center" Text="制单" FontSize="Small" BackgroundColor="Green" Clicked="OnAddClicked"/>
<Button Grid.Column="2" HorizontalOptions="Center" Text="编辑" FontSize="Small" BackgroundColor="YellowGreen" Clicked="OnEditClicked"/>
<Button Grid.Column="3" HorizontalOptions="Center" Text="删除" FontSize="Small" BackgroundColor="Red" Clicked="OnDeleteClicked"/>
</Grid>
</StackLayout>
</ContentPage>
3.3 控制器逻辑层
定义
private MO1002AllViewModel MO1002List { get; set; } = new MO1002AllViewModel();
界面绑定,也可以在view层绑定
BindingContext = MO1002List;
数据初始化,从服务端取数(可以不看,重点关注数据绑定)
/// <summary>
/// 刷新
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void OnRefreshClicked(object sender, EventArgs e)
{
try
{
//按工作日期、制单人查询
string str_Result = GycMobileService.Proxy.GetMasterTAX_2010(MO1002List.TitleBar.WorkDate, MO1002List.TitleBar.UserID);
var var_MobileResult = JsonConvert.DeserializeObject<MobileResult>(str_Result);
string aa = var_MobileResult.RetValue.ToString();
if (var_MobileResult.IsSuccess)
{
MO1002List.DataList.Clear();
List<Tax2010>? list_Tax2010 = JsonConvert.DeserializeObject<List<Tax2010>>(var_MobileResult.RetValue.ToString());
if (list_Tax2010 != null && list_Tax2010.Count > 0)
{
// 创建一个 MO1002AddViewModel 实例来获取线边库的名称
var addViewModel = new MO1002AddViewModel();
foreach (var item in list_Tax2010)
{
MO1002List.DataList.Add(new MO1002DetailsModel
{
CReservedNo = item.CReservedNo,
Creator = item.Creator,
CreatorCn = MO1002List.TitleBar.UserName,
//框架未知问题,创建日期数据返回不对
CreateTime = item.CreateTime,
CreateTimeText = item.CreateTime.ToString("yyyy-MM-dd HH:MM:ss"),
//CreateTime = item.LastModifyTime,
//CreateTimeText = item.LastModifyTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty,
CReservedType = item.CReservedType,
CReservedTypeCn = "退库",
CMtrlType = item.CMtrlType,
CMtrlTypeCn = addViewModel.GetMtrlTypeValueByKey(item.CMtrlType),
CSendStoreHouse = item.CSendStoreHouse,
CSendStoreHouseCn = addViewModel.GetHouseValueByKey(item.CSendStoreHouse),
CSendBnPlantId = item.CSendBnPlantId,
CSendBnPlantIdCn = addViewModel.GetBnPlantValueByKey(item.CSendBnPlantId),
CClassRate = item.CClassRate,
CRemark = item.CRemark,
CRemarkCn = addViewModel.GetReasonValueByKey(item.CRemark),
});
}
}
Toaster.Show(var_MobileResult.Remark);
}
else
{
await MessageExtension.Error(var_MobileResult.Remark);
}
}
catch (Exception ex)
{
Toaster.Show(ex.Message);
return;
}
}
获取选中的数据,从集合中取被勾选的数据行。
/// <summary>
/// 单选按钮选中事件
/// </summary>
/// <param name="sender"></param>
/// <param "e"></param>
private void OnRadioButtonCheckedChanged()
{
if (MO1002List.DataList != null && MO1002List.DataList.Count > 0)
{
MO1002List.SelectedDetail = MO1002List.DataList.FirstOrDefault(x => x.IsCheck == true) ?? new MO1002DetailsModel();
}
}
四、项目展示