The Joker
The Joker

Reputation: 402

WPF Property not updated in UI when modified via derived class

I am unable to display the CurrentStatus property from my ViewModelBase class in the status bar of my WPF application.

ViewModelBase is inherited by TasksViewModel and UserViewModel.

UserViewModel is inherited by ImportViewModel and TestViewModel.

MainWindow has a DataContext of TasksViewModel.

ViewModelBase:

    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        private string _currentStatus;

        public string CurrentStatus
        {
            get { return _currentStatus; }
            set
            {
                if (value == _currentStatus)
                {
                    return;
                }
                _currentStatus = value;
                OnPropertyChanged(nameof(CurrentStatus));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

TasksViewModel:

    public  class TasksViewModel : ViewModelBase
    {
        public IEnumerable<ViewModelBase> Collection => _collection;
        public override string ViewModelName => "Tasks";
        public TasksViewModel()
        {
            _collection = new ObservableCollection<ViewModelBase>
                          {
                              new ImportUsersViewModel(),
                              new TestFunctionsViewModel()
                          };

            // Added as per John Gardner's answer below.
            // watch for currentstatus property changes in the internal view models and use those for our status
            foreach (ViewModelBase i in _collection)
            {
                i.PropertyChanged += InternalCollectionPropertyChanged;
            }
        }

        // Added as per John Gardner's answer.
        private void InternalCollectionPropertyChanged(object source, PropertyChangedEventArgs e)
        {
            var vm = source as ViewModelBase;
            if (vm != null && e.PropertyName == nameof(CurrentStatus))
            {
                CurrentStatus = vm.CurrentStatus;
            }
        }
      }

ImportUsersViewModel:

internal class ImportUsersViewModel : UserViewModel
{
    private async void BrowseInputFileAsync()
    {
        App.Log.Debug("Browsing for input file.");
        string path = string.IsNullOrWhiteSpace(InputFile)
                          ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
                          : Path.GetDirectoryName(InputFile);
        InputFile = FileFunctions.GetFileLocation("Browse for User Import File",
                                                  path, FileFunctions.FileFilter.CSVTextAll) ?? InputFile;

        CurrentStatus = "Reading Import file.";
        ImportUsers = new ObservableCollection<UserP>();
        ImportUsers = new ObservableCollection<User>(await Task.Run(() => ReadImportFile()));
        string importResult =
            $"{ImportUsers.Count} users in file in {new TimeSpan(readImportStopwatch.ElapsedTicks).Humanize()}.";
        CurrentStatus = importResult; // Property is updated in ViewModelBase, but not in UI.
    }
}

MainWindow.xaml:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:viewModel="clr-namespace:ViewModel"
    xmlns:view="clr-namespace:Views"
    x:Class="MainWindow"
    Title="Users"
    Height="600"
    Width="1000"
    Icon="Resources/Icon.png">
    <Window.Resources>
        <DataTemplate DataType="{x:Type viewModel:ImportUsersViewModel}">
            <view:ImportUsers />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:TestFunctionsViewModel}">
            <view:TestFunctionsView />
        </DataTemplate>
    </Window.Resources>
    <Window.DataContext>
        <viewModel:TasksViewModel />
    </Window.DataContext>
    <DockPanel>
        <StatusBar DockPanel.Dock="Bottom" Height="auto">
            <TextBlock Text="Status: " />
            <!-- Not updated in UI by any View Model -->
            <TextBlock Text="{Binding CurrentStatus}" />
        </StatusBar>
    </DockPanel>
</Window>

If I bind a text block to the CurrentStatus property inside the ImportUsers UserControl it updates without issue, but the "parent" status bar does not update.

My suspicion is that it can't be displayed in the MainWindow status bar because, although both ImportViewModel and TasksViewModel inherit ViewModelBase, they don't have any link to each other, and the TasksViewModel CurrentStatus property isn't being updated.

Upvotes: 0

Views: 1088

Answers (2)

John Gardner
John Gardner

Reputation: 25126

Your window's DataContext is a TaskViewModel, but nothing on that view model is watching for property changes in it's collection, and updating itself. essentially, TasksViewModel is containing the other viewmodels, but not aggregating any of their behaviors.

you need something like:

public  class TasksViewModel : ViewModelBase
{
    public IEnumerable<ViewModelBase> Collection => _collection;
    public override string ViewModelName => "Tasks";
    public TasksViewModel()
    {
        _collection = new ObservableCollection<ViewModelBase>
                      {
                          new ImportUsersViewModel(),
                          new TestFunctionsViewModel()
                      };

        // watch for currentstatus property changes in the internal view models and use those for our status
        foreach (var i in _collection)
        {
             i.PropertyChanged += this.InternalCollectionPropertyChanged;
        }
    }
  }

  //
  // if a currentstatus property change occurred inside one of the nested
  // viewmodelbase objects, set our status to that status
  //
  private InternalCollectionPropertyChanged(object source, PropertyChangeEvent e)
  {
      var vm = source as ViewModelBase;
      if (vm != null && e.PropertyName = nameof(CurrentStatus))
      {
           this.CurrentStatus = vm.CurrentStatus;
      }
  }

Upvotes: 1

benPearce
benPearce

Reputation: 38333

I think your suspicion is correct.

The DataContext on the Window is a different ViewModel instance to that of the ImportUsersViewModel.

While CurrentStatus is defined in the same object hierarchy, the CurrentStatus line in the ImportUsersViewModel is changing a different object instance than the CurrentStatus property attached to the Window DataContext.

Upvotes: 1

Related Questions