1

I am learning WPF for work and working on a simple Todo List application, and have run into some problems with the Observable List not updating the view.

I am using the MVVM Light framework, and have done a test subscription to the CollectionChanged event which does get fired when I add elements. I am doing multiple views, so in the off chance that I am not in the main UI thread I've added the MVVM Light's DispatcherHelper to ensure it is being invoked on the main thread. I've also tried manually re-triggering the INotifyPropertyChanged by using MVVM Light's RaisePropertyChanged function on the observable list (see the commented out code). To ensure it was not me screwing up the XAML I also changed the ListBox to a DataGrid with the same results.

Here are a list of some of the stack overflow messages I tried and tutorials I've followed trying to get this done:

Todo Model Class

namespace WPFTodoList.Models
{
    class TodoItem : ObservableObject
    {
        public String Title { get { return this._title; } set
            {
                Set(() => this.Title, ref this._title, value);
            }
        }
        public String Description { get { return this._description; } set
            {
                Set(() => this.Description, ref this._description, value);
            }
        }
        public int Priority { get { return this._priority; } set
            {
                Set(() => this.Priority, ref this._priority, value);
            }
        }
        public bool Done { get { return this._done; } set
            {
                Set(() => this._done, ref this._done, value);
            }
        }


        private String _title;
        private String _description;
        private int _priority;
        private bool _done;
    }
}

View Model Class

class TodoListAppViewModel : ViewModelBase
    {

        public ObservableCollection<TodoItem> Todos { get; private set; }

        public RelayCommand AddTodoItemCommand { get{ return this._addTodoItemCommand ?? this._BuildAddTodoItemCommand(); } }

        private RelayCommand _addTodoItemCommand;
        private NavigationService _navService;
        private TodoService _todoService;
        private IDisposable _addTodoSubscription;

        public TodoListAppViewModel(NavigationService nav, TodoService todo)
        {
            this._navService = nav;
            this._todoService = todo;
            this.InitViewModel();
        }

        public TodoListAppViewModel()
        {
            this._navService = NavigationService.GetInstance();
            this._todoService = TodoService.GetInstance();
            this.InitViewModel();
        }

        private void InitViewModel()
        {
            this.Todos = new ObservableCollection<TodoItem>();
            this.Todos.CollectionChanged += this.CollectionChanged;
            this._addTodoSubscription = this._todoService.AddTodoItem.Subscribe((value) =>
            {
                /*this.Todos.Add(value);
                RaisePropertyChanged(() => this.Todos);*/
                DispatcherHelper.CheckBeginInvokeOnUI(() => this.Todos.Add(value));
            });
        }

        private RelayCommand _BuildAddTodoItemCommand()
        {
            this._addTodoItemCommand = new RelayCommand( () => this._navService.NavigateTo("addEdit"));
            return this._addTodoItemCommand;
        }

        private void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine(e.Action.ToString());
        }

        ~TodoListAppViewModel()
        {
            this._addTodoSubscription.Dispose();
        }
    }
}

View XAML

<UserControl x:Class="WPFTodoList.Views.TodoListAppView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WPFTodoList.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <DataTemplate x:Key="TodoItemTemplate">
            <Label Content="{Binding Title}"/>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="8*" MaxHeight="180px"/>
            <RowDefinition Height="16*"/>
        </Grid.RowDefinitions>
        <StackPanel Margin="24,-20,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Panel.ZIndex="5">
            <Label Content="Your" Foreground="White" FontSize="16" />
            <Label Content="Todo List" Foreground="White" FontSize="24" Margin="0,-15,0,0" />
        </StackPanel>
        <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5">
            <Rectangle.Fill>
                <RadialGradientBrush GradientOrigin="0.2,0.1" RadiusX="1" RadiusY="1" SpreadMethod="Reflect" MappingMode="RelativeToBoundingBox" Center="0.1,0.1">
                    <GradientStop Color="#FF5B447C" Offset="0.329"/>
                    <GradientStop Color="#FF1D1F5A" Offset="1"/>
                </RadialGradientBrush>
            </Rectangle.Fill>
        </Rectangle>
        <Rectangle Fill="#3FFFFFFF" VerticalAlignment="Bottom" Height="40" Panel.ZIndex="5"/>
        <StackPanel Panel.ZIndex="10" Height="40" HorizontalAlignment="Right" VerticalAlignment="Bottom" Orientation="Horizontal">
            <Button Background="Transparent" BorderBrush="Transparent" HorizontalAlignment="Right" Command="{Binding AddTodoItemCommand}" BorderThickness="0,0,0,0" SnapsToDevicePixels="True" >
                <Button.ToolTip>
                    <Label Content="Add A Todo Item"/>
                </Button.ToolTip>
                <Image Source="../Resources/add.png" HorizontalAlignment="Right" />
            </Button>
        </StackPanel>
        <ListBox Grid.Row="1"  ItemsSource="{Binding Todos}" ItemTemplate="{DynamicResource TodoItemTemplate}"/>
    </Grid>
</UserControl>

View Code Back

public partial class TodoListAppView : UserControl
    {
        public TodoListAppView()
        {
            InitializeComponent();
            TodoListAppViewModel vm = new TodoListAppViewModel();
            this.DataContext = vm;
        }

    }

To simulate the todo without another 4-6 files I used a RX.net subject to relay the todo item from the other view to the list view (I can post code if requested).

As for how the window displays the views here is the root xaml

<Window x:Class="WPFTodoList.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFTodoList"
        mc:Ignorable="d"
        xmlns:views="clr-namespace:WPFTodoList.Views"
        xmlns:viewModels="clr-namespace:WPFTodoList.ViewModels"
        Title="MainWindow" Height="600" Width="300">
    <Window.Resources>
        <DataTemplate x:Name="AddEditPage" DataType="{x:Type viewModels:AddEditTodoViewModel}">
            <views:AddEditTodoView DataContext="{Binding}"/>
        </DataTemplate>
        <DataTemplate x:Name="TodoListApp" DataType="{x:Type viewModels:TodoListAppViewModel}">
            <views:TodoListAppView DataContext="{Binding}"/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ContentControl Content="{Binding ActivePage}"/>
    </Grid>
</Window>

ActivePage is then bound to an instance of TodoListAppViewModel

The expected behavior is when the subscription to the TodoService.AddTodoItem subscription is fired, the provided instance of a TodoListItem passed to the lambda should be added to the observable Todos list. The list in turn should update the view. Right now I am seeing the CollectionChanged event being fired inside of TodoListAppViewModel, however the view does not update.

Clemens
  • 123,504
  • 12
  • 155
  • 268
Dragonman117
  • 478
  • 4
  • 13
  • 2
    The problem is at the line `this.DataContext = vm;`. Doing this breaks any DataContext-based Bindings of the UserControl's properties, like `DataContext="{Binding}"`. A Binding like this (usually with a property path) looks up the source property in the current DataContext, which you have set to a private view model instance. A UserControl should never explicitly set its own DataContext, and therefore never have its own, private view model. – Clemens Oct 05 '19 at 06:31
  • 2
    If you just remove the DataContext assignment from the UserControl's constructor, everything should just work. You may then also remove `DataContext="{Binding}"`, because it is entirely redundant. It does nothing but set the DataContext property to the value that it already has. – Clemens Oct 05 '19 at 06:35

0 Answers0