我正在尝试对我的其他问题实施建议的解决方案。目标是使用数据绑定代替处理
ComboBox
Loaded
事件。
我有两个嵌套的 ViewModel,我正在尝试数据绑定(类似于 这里的简化问题),其中
ListView
显示外部 ViewModel (TaskViewModel
) 的列表,而 ComboBox
内部的 ListView
显示内部 ViewModel (StatusViewModel
) 的列表,并且 SelectedItem
内部的 ComboBox
是 TwoWay
数据绑定到 Status
上的 TaskViewModel
属性。
我不断收到意外的未捕获异常,这是由
Set
上的 TaskViewModel.Status
设置空值引起的。当使用 Visual Studio StackTrace 时,我只能发现这个 setter 是从“外部代码”调用的。
如果我取消注释 TaskViewModel.cs 中注释掉的代码,代码会运行,但 ComboBox 绑定不会执行任何操作。 我使用 INotifyPropertyChanged
上的
TaskViewModel.Status
实现了嵌套视图模型的 问题的解决方案,但是这似乎没有解决我的问题。
这个空值从何而来?我已验证进入
MyTask
的 SetProjectTasks()
列表从未包含具有 Status
值 null
的任务。
实现此功能的正确方法是什么(绑定到
ListView
的外部视图模型列表,该视图模型上的嵌套视图模型属性绑定到 ComboBox
)?难道是我的做法不对?
页面.xaml
<ListView x:Name="TasksListView"
Grid.Row="1"
Grid.ColumnSpan="2"
ItemsSource="{x:Bind MyTasks}"
SelectionMode="None"
IsItemClickEnabled="True"
ItemClick="TasksListView_ItemClick">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:TaskViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ComboBox x:Name="StatusComboBox"
Tag="{x:Bind ID, Mode=OneWay}"
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
ItemsSource="{Binding Path=ProjectTaskStatuses, ElementName=RootPage}"
SelectedValue="{x:Bind Status, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="viewmodels:StatusViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0"
Margin="0,0,10,0"
Height="10"
Width="10"
StrokeThickness="1">
<Rectangle.Fill>
<SolidColorBrush Color="{x:Bind Color}"></SolidColorBrush>
</Rectangle.Fill>
<Rectangle.Stroke>
<SolidColorBrush Color="{x:Bind Color}"></SolidColorBrush>
</Rectangle.Stroke>
</Rectangle>
<TextBlock Grid.Column="1"
Text="{x:Bind Name}"></TextBlock>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Column="1"
Text="{x:Bind Name}"></TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
页面.xaml.cs
public ObservableCollection<StatusViewModel> ProjectTaskStatuses { get; set; }
private ObservableCollection<TaskViewModel> MyTasks { get; set; }
public void SetProjectStatuses(List<Status> statuses)
{
this.ProjectTaskStatuses.Clear();
statuses.ForEach(status => this.ProjectTaskStatuses.Add(new StatusViewModel(status)));
}
public void SetProjectTasks(List<MyTask> tasks)
{
this.MyTasks.Clear();
tasks.ForEach(task => this.MyTasks.Add(new TaskViewModel(task)));
}
TaskViewModel.cs
public class TaskViewModel : INotifyPropertyChanged
{
private MyTask _model;
public MyTask Model
{
get => new MyTask(this._model) { Status = this._status.Model };
}
public string ID
{
get => this._model?.ID;
set
{
this._model.ID = value;
this.RaisePropertyChanged(nameof(ID));
}
}
public string Name
{
get => this._model?.Name;
set
{
this._model.Name = value;
this.RaisePropertyChanged(nameof(Name));
}
}
private StatusViewModel _status;
public Status Status
{
get => this._status?.Model;
set
{
// COMMENTED OUT CODE FOR TESTING - THIS IS WHERE THE UNEXPECTED NULL HAPPENS
//if (value == null)
//{
// System.Diagnostics.Debug.WriteLine("NULL STATUS BEING SET TO - " + this._model.ID + " " + this._model.Name + " " + this._model.Status.Name);
// return;
//}
if (this._status != null)
this._status.PropertyChanged -= StatusChanged;
this._status = new StatusViewModel(value);
if (this._status != null)
this._status.PropertyChanged += StatusChanged;
this.RaisePropertyChanged(nameof(Status));
void StatusChanged(object sender, PropertyChangedEventArgs e) => this.RaisePropertyChanged(nameof(Status));
}
}
/// <summary>
/// Raised when a bindable property of the viewmodel has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public TaskViewModel(MyTask task)
{
this._model = task;
this._status = new StatusViewModel(task.Status);
}
}
StatusViewModel.cs
public class StatusViewModel : INotifyPropertyChanged
{
private Status _model;
public Status Model
{
get => new Status(this._model);
}
public string ID
{
get => this._model?.ID;
set
{
this._model.ID = value;
this.RaisePropertyChanged(nameof(ID));
}
}
public string Name
{
get => this._model?.Name;
set
{
this._model.Name = value;
this.RaisePropertyChanged(nameof(Name));
}
}
public Color Color
{
get => this._model.Color;
set
{
this._model.Color = value;
this.RaisePropertyChanged(nameof(Color));
}
}
/// <summary>
/// Raised when a bindable property of the viewmodel has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public StatusViewModel(Status status)
{
this._model = status;
}
}
很难用所有这些代码来判断。这是我可以发现的两个问题。
ComboBox
的ItemsSource
是StatusViewModel
的集合,但SelectedValue
绑定到Status
。Model
中的StatusViewModel
属性始终返回一个新实例。这可能不是答案,但让我通过使用 CommunityToolkit.Mvvm NuGet 包向您展示一些接近但代码更少的内容。
public class Status
{
public string? ID { get; internal set; }
public string? Name { get; internal set; }
public Color Color { get; internal set; }
}
public class MyTask
{
public string? ID { get; internal set; }
public string? Name { get; internal set; }
public Status? Status { get; set; }
}
public partial class TaskViewModel : ObservableObject
{
[ObservableProperty]
private MyTask _model;
public TaskViewModel(MyTask task)
{
this._model = task;
}
}
public sealed partial class MainPage : Page
{
public MainPage()
{
InitializeComponent();
}
public ObservableCollection<Status> ProjectTaskStatuses { get; set; } = new();
private ObservableCollection<TaskViewModel> MyTasks { get; set; } = new();
public void SetProjectStatuses(List<Status> statuses)
{
ProjectTaskStatuses.Clear();
statuses.ForEach(status => this.ProjectTaskStatuses.Add(status));
}
public void SetProjectTasks(List<MyTask> tasks)
{
MyTasks.Clear();
tasks.ForEach(task => this.MyTasks.Add(new TaskViewModel(task)));
}
private void SetTasksButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
List<MyTask> tasks = new();
Status? defaultStatus = ProjectTaskStatuses.FirstOrDefault();
tasks.Add(new MyTask { ID = "1", Name = "Task 1", Status = defaultStatus });
tasks.Add(new MyTask { ID = "2", Name = "Task 2", Status = defaultStatus });
tasks.Add(new MyTask { ID = "3", Name = "Task 3", Status = defaultStatus });
SetProjectTasks(tasks);
}
private void SetStatusOptionsButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
List<Status> statuses = new();
statuses.Add(new Status { ID = "1", Name = "Status 1", Color = Colors.Red });
statuses.Add(new Status { ID = "2", Name = "Status 2", Color = Colors.Green });
statuses.Add(new Status { ID = "3", Name = "Status 3", Color = Colors.Blue });
SetProjectStatuses(statuses);
}
}
<Grid RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<Button
Click="SetStatusOptionsButton_Click"
Content="Set status options" />
<Button
Click="SetTasksButton_Click"
Content="Set taks" />
</StackPanel>
<ListView
x:Name="TasksListView"
Grid.Row="1"
IsItemClickEnabled="True"
ItemsSource="{x:Bind MyTasks}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:TaskViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox
Grid.Column="0"
ItemsSource="{Binding ElementName=RootPage, Path=ProjectTaskStatuses}"
SelectedItem="{x:Bind Model.Status, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="local:Status">
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Column="1"
Text="{x:Bind Model.Name, Mode=OneWay}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>