I am attempting to implement a proposed solution to my other question here. The goal is to use databinding in place of handling ComboBox
Loaded
events.
I have two nested ViewModels I am trying to databind (similar to the simplified question here), where a ListView
displays a list of the outter ViewModel (TaskViewModel
) while the ComboBox
inside of the ListView
displays a list of the inner ViewModel (StatusViewModel
), and the SelectedItem
inside of the ComboBox
is TwoWay
databound to the Status
property on the TaskViewModel
.
I keep getting an unexpected uncaught exception, which is being cause by the Set
on TaskViewModel.Status
setting a null value. When using the Visual Studio StackTrace, all I can find is that this setter is being called from "External Code".
If I uncomment the commented out code in TaskViewModel.cs, the code runs but the ComboBox binding does nothing. I implemented the solution to the question here for nested view models with INotifyPropertyChanged
on my TaskViewModel.Status
, but that did not seem to fix my issue.
Where is this null value coming from? I have verified that the list of MyTask
going into SetProjectTasks()
never contains a task with Status
value null
.
What is the proper way to implement this (list of outer view models bound to ListView
with nested view model property on that view model being bound to ComboBox
)? Is my approach wrong?
Page.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>
Page.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;
}
}
UPDATE
I added the CommunityToolkit.Mvvm
NuGet package to my project, and replaced the MyTask
model as suggested. I also eliminated the StatusViewModel
entirely and replaced it with just Status
. This simplified the code, but same problem. I subscribed to the PropertyChanged
event with a print statement in the handler, and confirm that it is never being fired:
Code behind changes:
public partial class TaskViewModel : ObservableObject
{
[ObservableProperty]
private MyTask _model;
public TaskViewModel(MyTask task)
{
this._model = task;
}
}
public void SetProjectTasks(List<MyTask> tasks)
{
MyTasks.Clear();
TaskViewModel taskViewModel = new TaskViewModel(task)
taskViewModel.PropertyChanged += this.ViewModel_PropertyChanged;
tasks.ForEach(task => this.MyTasks.Add(taskViewModel ));
}
private void TaskViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Confirmed with debugger and print statement this event handler never runs... even when changing the status ComboBox.
TaskViewModel model = sender as TaskViewModel;
System.Diagnostics.Debug.WriteLine("Model for task: " + model.Model.Name +
" property " + e.PropertyName +
" changed to " + model.GetType().GetProperty(e.PropertyName).GetValue(model).ToString());
}
XAML changes:
<ComboBox x:Name="StatusComboBox"
Tag="{x:Bind Model.ID, Mode=OneWay}"
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
ItemsSource="{Binding Path=ProjectTaskStatuses, ElementName=RootPage}"
SelectedValue="{x:Bind Model.Status, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:Status">
<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>
Issues:
- The
ComboBox
is not initialized - there is no selection. - Changing the
ComboBox
(by clicking on it and selecting a different value) does not trigger any code to run. - The
ComboBox
data binding appears to be over-writing theStatus
value tonull
at some point. I have confirmed theMyTask.Status
values being set inSetProjectTasks()
still never has a null value.