.NET Maui 中绑定值变化后,Entry 字段为何不更新?
问题主要出在缺少属性变更通知。HomePageViewModel 虽然继承了 ObservableObject,普通自动属性却不会因此自动发送通知;Patient 中的 Lastname、Id 等属性也没有实现通知机制。
姓名字段还绑定了整个 MyPatient 对象。即使显示时调用了 ToString(),绑定系统也无法据此判断姓名依赖哪些属性。需要将它绑定到具体的姓名属性,并在相关数据变化时发送通知。
为什么初始值能显示,后续修改却没有反应?
绑定建立时会读取当前值,所以启动时的患者信息可以正常显示。后续值发生变化时,需要通过 INotifyPropertyChanged.PropertyChanged 通知绑定系统重新读取。
替换患者对象和修改对象内部的属性,需要分别处理:
MyPatient = anotherPatient:由HomePageViewModel通知MyPatient已更换。MyPatient.Id = 2:由当前Patient对象通知Id已改变。
原代码在这两个层级都使用了普通自动属性,因此没有发出相应通知。x:DataType 用于编译绑定的类型检查,不会替属性实现变更通知。
1. 让 Patient 的属性发送通知
项目已经使用 CommunityToolkit.Mvvm,可以用 ObservableObject 和 SetProperty 实现通知,再增加一个 FullName 属性,保留原来“姓, 名”的显示格式:
using CommunityToolkit.Mvvm.ComponentModel;
namespace Aequus.CoreBusiness;
public class Patient : ObservableObject
{
private int id;
private string lastname = string.Empty;
private string firstname = string.Empty;
public int Id
{
get => id;
set => SetProperty(ref id, value);
}
public string Lastname
{
get => lastname;
set
{
if (SetProperty(ref lastname, value))
{
OnPropertyChanged(nameof(FullName));
}
}
}
public string Firstname
{
get => firstname;
set
{
if (SetProperty(ref firstname, value))
{
OnPropertyChanged(nameof(FullName));
}
}
}
public string FullName => $"{Lastname}, {Firstname}";
public override string ToString() => FullName;
}
SetProperty 会比较新旧值,在值发生变化时发送对应属性的 PropertyChanged 事件。FullName 依赖 Lastname 和 Firstname,所以这两个属性变化时,还要发送 FullName 的变更通知。
使用这段代码时,Aequus.CoreBusiness 项目需要引用 CommunityToolkit.Mvvm。如果不希望业务层依赖该包,可以自行实现 INotifyPropertyChanged,也可以在界面层增加支持属性变更通知的患者 ViewModel。
2. 让 MyPatient 的替换也能通知界面
MyPatient 同样需要通过 SetProperty 赋值,这样异步加载完成后,界面才能读取新的患者对象:
using Aequus.CoreBusiness;
using Aequus.UseCases.Interfaces;
using CommunityToolkit.Mvvm.ComponentModel;
using DicomObjects;
using Microsoft.Maui.Controls;
namespace Aequus.Maui.ViewModels;
public class HomePageViewModel : ObservableObject, IQueryAttributable
{
private Patient? myPatient;
private DicomImage? currentDicomImage;
public Patient? MyPatient
{
get => myPatient;
set => SetProperty(ref myPatient, value);
}
public ILoadPatientUseCase LoadPatientUseCase { get; }
public HomePageViewModel(ILoadPatientUseCase loadPatientUseCase)
{
LoadPatientUseCase = loadPatientUseCase;
}
public async Task LoadPatientAsync()
{
MyPatient = await LoadPatientUseCase.ExecuteAsync();
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (!query.TryGetValue("ReturnedObject", out var obj))
{
return;
}
// 保留原问题的测试逻辑,允许返回值暂时为 null。
currentDicomImage = obj as DicomImage;
if (MyPatient is null)
{
return;
}
MyPatient.Lastname = "Peters";
MyPatient.Id = 2;
}
}
修改后,MyPatient.Lastname 和 MyPatient.Id 的 setter 会发送通知,每次赋值后都不必再手动刷新页面。
这里的两条赋值仍用于测试。实际导入时,应根据返回的图像更新患者数据,并按业务要求检查 obj 是否为有效的 DicomImage。
3. 姓名绑定到 FullName,ID 绑定到 Id
如果两个字段只用于显示,可以改为:
<Entry
Grid.Row="0"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding MyPatient.FullName, Mode=OneWay}" />
<Entry
Grid.Row="1"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding MyPatient.Id, Mode=OneWay}" />
修改 Lastname 时,FullName 的变更通知会让第一个字段更新;修改 Id 时,第二个字段会读取新的 ID。
如果姓名字段只需要显示姓,可以直接绑定:
<Entry
Text="{Binding MyPatient.Lastname, Mode=TwoWay}" />
FullName 是只读计算属性,适合展示。要编辑完整姓名,应分别绑定 Lastname 和 Firstname,或提供明确的姓名编辑逻辑。整数 ID 如果允许编辑,还需要处理空字符串和无效输入;Keyboard="Numeric" 不能代替数据校验。
4. 从 ImportPage 返回时只执行一次返回导航
原代码先导航到绝对路由:
await Shell.Current.GoToAsync($"//{nameof(HomePage)}");
随后又执行:
await Shell.Current.GoToAsync("..", navigationParameter);
第一次导航已经改变了当前页面和导航栈,第二次的 .. 就不再表示“从 ImportPage 返回 HomePage”。具体结果取决于 Shell 路由结构。
如果 ImportPage 是从 HomePage 压入导航栈的,返回并传参只需一次操作:
private async void btnImport_Clicked(object sender, EventArgs e)
{
if (currentDicomImage is null)
{
return;
}
var navigationParameter = new Dictionary<string, object>
{
["ReturnedObject"] = currentDicomImage
};
await Shell.Current.GoToAsync("..", navigationParameter);
}
private async void btnCancel_Clicked(object sender, EventArgs e)
{
await Shell.Current.GoToAsync("..");
}
导入成功后返回对象,取消时直接返回,两种操作分别处理。
如果只是测试通知机制,还没有选择图像,这段导入代码会因为 currentDicomImage 为 null 而直接返回。可以先在患者加载完成后临时执行两条测试赋值,确认绑定能否更新。
5. 单独处理异步初始化
原构造函数调用了 LoadPatientAsync(),却没有等待它完成。这样难以控制执行顺序和处理加载异常,也可能在收到返回参数时,MyPatient 还没有加载完成。
前面的 ViewModel 已移除构造函数中的异步调用,需要在页面初始化时显式等待加载完成。可以保留现有的 BindingContext 设置,在 HomePage 中增加:
private bool isLoading;
private bool isLoaded;
protected override async void OnAppearing()
{
base.OnAppearing();
if (isLoading || isLoaded)
{
return;
}
isLoading = true;
try
{
await homePageViewModel.LoadPatientAsync();
isLoaded = true;
}
catch (Exception ex)
{
await DisplayAlert("加载失败", ex.Message, "OK");
}
finally
{
isLoading = false;
}
}
private async void ImportToolBar_Clicked(object sender, EventArgs e)
{
if (!isLoaded)
{
return;
}
await Shell.Current.GoToAsync(nameof(ImportPage));
}
返回页面时,OnAppearing 还会执行。这里用 isLoaded 避免重复加载,以免覆盖刚导入的数据。实际界面也可以在加载期间禁用导入按钮。
如果修改后仍显示旧值,先检查 ApplyQueryAttributes 是否收到了参数,再确认它所属的 ViewModel 就是当前页面的 BindingContext。为已有对象补发 OnPropertyChanged(nameof(MyPatient)) 可以帮助诊断,或临时让绑定重新求值。持续修改嵌套属性时,应由 Patient 自身发送对应属性的变更通知。
备注:内容仅供参考。
