如何在 WinUI 3 中以 TwoWay 数据绑定嵌套 INotifyPropertyChanged ViewModel 到 ListView 内的 ComboBox?

问题描述 投票:0回答:1

我正在尝试对我的其他问题实施建议的解决方案。目标是使用数据绑定代替处理

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;
    }
}
c# listview data-binding combobox winui-3
1个回答
0
投票

很难用所有这些代码来判断。这是我可以发现的两个问题。

  • 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>
© www.soinside.com 2019 - 2024. All rights reserved.