Обновить свойство объекта после изменения свойства в BindingList

Я новичок в WPF и MVVM, и сейчас это меня очень расстраивает. Моя текущая проблема:

У меня есть следующий XAML:

<Window.DataContext>
    <local:MainViewModel />
</Window.DataContext>

<WrapPanel Orientation = "Horizontal">
    <ComboBox ItemsSource = "{Binding StreetList}" SelectedItem = "{Binding SelectedStreet}" DisplayMemberPath = "StreetName" IsSynchronizedWithCurrentItem = "True"/>
    <DataGrid ItemsSource = "{Binding SelectedStreet.HouseList}" AutoGenerateColumns = "False" >
        <DataGrid.Columns>
            <DataGridTextColumn Header = "YearBuilt" Binding = "{Binding YearBuilt}" />
            <DataGridTextColumn Header = "Price" Binding = "{Binding Price}" />
            <DataGridTextColumn Header = "#Rooms" Binding = "{Binding Rooms}" />
        </DataGrid.Columns>
    </DataGrid>
    <TextBox Text = "{Binding AvgPricePerRoom, Mode=OneWay}" />
</WrapPanel>

где:

public class MainViewModel : ViewModelBase
{
    public BindingList<StreetViewModel> StreetList { get { return _streetList; } }
    private BindingList<StreetViewModel> _streetList;

    public StreetViewModel SelectedStreet
    {
        get { return _selectedStreet; }
        set { _selectedStreet = (StreetViewModel)value.Clone(); RaisePropertyChanged(); RaisePropertyChanged(nameof(AvgPricePerRoom)); }
    }
    private StreetViewModel _selectedStreet;

    public double AvgPricePerRoom
    {
        get
        {
            return
              _selectedStreet.HouseList[0].Price / _selectedStreet.HouseList[0].Rooms / 3 +
              _selectedStreet.HouseList[1].Price / _selectedStreet.HouseList[1].Rooms / 3 +
              _selectedStreet.HouseList[2].Price / _selectedStreet.HouseList[2].Rooms / 3;
        }
    }

    public MainViewModel()
    {
        _streetList = new BindingList<StreetViewModel>();
        fill_HouseGroupList();
    }

    void fill_HouseGroupList() { ... }
}

и (РЕДАКТИРОВАТЬ):

public class StreetViewModel : ViewModelBase, ICloneable
{
    public string StreetName
    {
        get { return _streetName; }
        set { _streetName = value; RaisePropertyChanged(); }
    }
    private string _streetName;


    private BindingList<HouseViewModel> _houseList;

    private void HouseList_ListChanged(object sender, ListChangedEventArgs e)
    {
        throw new NotImplementedException();
    }

    public StreetViewModel() { }

    public StreetViewModel(string streetName) : this()
    {
        _streetName = streetName;

        _houseList = new BindingList<HouseViewModel>();

        HouseList.ListChanged += HouseList_ListChanged;
    }

    public BindingList<HouseViewModel> HouseList
    {
        get
        {
            if (_houseList.Count == 0)
            {
                switch (_streetName)
                {
                    case "Main street":
                        _houseList.Add(new HouseViewModel(new House(2015, 600, 9)));
                        _houseList.Add(new HouseViewModel(new House(1929, 20, 4)));
                        _houseList.Add(new HouseViewModel(new House(1969, 30, 6)));
                        break;
                    case "School street":
                        _houseList.Add(new HouseViewModel(new House(2017, 50, 10)));
                        _houseList.Add(new HouseViewModel(new House(1930, 10, 5)));
                        _houseList.Add(new HouseViewModel(new House(1970, 20, 7)));
                        break;
                    case "Garden street":
                        _houseList.Add(new HouseViewModel(new House(2014, 1, 15)));
                        _houseList.Add(new HouseViewModel(new House(1935, 20, 11)));
                        _houseList.Add(new HouseViewModel(new House(1978, 20, 9)));
                        break;
                }
            }
            return _houseList;
        }
    }

    public object Clone()
    {
        var houseGroupCopy = new StreetViewModel(this._streetName);

        houseGroupCopy._houseList = new BindingList<HouseViewModel>();
        foreach (var house in _houseList)
        {
            houseGroupCopy._houseList.Add(new HouseViewModel(new House(house.YearBuilt, house.Price, house.Rooms)));
        }

        return houseGroupCopy;
    }
}

плюс:

public class HouseViewModel : ViewModelBase
{
    public House House
    {
        get { return _house; }
        set { _house = value;  RaisePropertyChanged(); }
    }
    private House _house;

    public int YearBuilt
    {
        get { return _house.YearBuilt; }
        set { _house.YearBuilt = value; RaisePropertyChanged(); }
    }

    public double Price
    {
        get { return _house.Price; }
        set { _house.Price = value; RaisePropertyChanged(); }
    }

    public int Rooms
    {
        get { return _house.Rooms; }
        set { _house.Rooms = value; RaisePropertyChanged(); }
    }

    public HouseViewModel(House house)
    {
        _house = house;
    }
}

... поэтому в DataGrid моего представления я могу редактировать свойства элементов House в моем HouseList, который является BindingList. Как я могу сделать так, чтобы свойство AvgPricePerRoom, которое отображается в TextBox, знало об изменениях в DataGrid, чтобы обновленный AvgPricePerRoom отображался при изменениях в Grid? Я использовал BindingList, потому что читал, что в отличие от ObservableCollection он вызывает событие также при изменении свойств его элементов (не считая проблем с производительностью), но как мне обработать это событие и уведомить свой AvgPricePerRoom в этом примере?

(Не весь приведенный выше код важен для моей проблемы, и я сожалею об этом ...)

Несвязанный совет: лучше RaisePropertyChanged("AvgPricePerRoom");, чем RaisePropertyChanged(nameof(AvgPricePerRoom));. Это позволяет избежать наличия строки, представляющей имя свойства, и риска того, что одно может измениться без изменения другого.

Richardissimo 14.05.2018 23:15

Еще один совет: обратите внимание, что свойство HouseList проверяет, есть ли _houseList==null, и если это так, оно пытается вызвать Add для нулевого объекта. Так что, если это действительно возможно, у вас появится исключение NullReferenceException.

Richardissimo 14.05.2018 23:20

спасибо @Richardissimo, я редактировал код

Sebastian 14.05.2018 23:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
545
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

При связывании коллекций существует 3 вида уведомлений об изменениях, о которых необходимо позаботиться:

  1. Уведомления о добавлении / удалении из списка. Это единственное уведомление, о котором действительно заботится ObservableCollection.
  2. Уведомления для свойства, предоставляющего ObservableCollection (StreetList в вашем примере). OC очень плохо справляются с крупномасштабными изменениями (без функции AddRange), поэтому часто лучше создать новый список в коде, выставив его только как последний шаг.
  3. Уведомление об изменении каждого свойства StreetViewModel.

Мой код, вероятно, слишком вложен, но я сосредотачиваюсь на BindingList<HouseViewModel> HouseList, который представлен DataGrid - и именно на вашем пункте 3. Как TextBox с AvgPricePerRoom всегда будет показывать обновленный результат на основе каждого изменения в DataGrid?

Sebastian 14.05.2018 23:37

После того, как у вас есть надлежащее уведомление об изменении, класс Binding позаботится о деталях. Все, что он действительно делает, это регистрирует необходимые события на обоих концах (ViewModel и View) и назначает изменение одному концу на другом (если TwoWayMode).

Christopher 14.05.2018 23:40

Также вы можете превратить AvgPricePerRoom в полноценное свойство с резервным полем. Сейчас это свойство только для получения с вычисленным возвращаемым значением, которое требует, чтобы вы вызывали его ChangeNotification из другого места. Вместо этого создайте функцию «CalculateAverage», которая вычисляет среднее значение и присваивает его свойству, а свойство повышает свое собственное уведомление об изменении.

Christopher 15.05.2018 00:01

Итак, я отредактировал код, и теперь мой HouseList должен отслеживать изменения в своих элементах через HouseList.ListChanged + = HouseList_ListChanged ;. Однако, когда элементы редактируются в сетке данных, обработчик HouseList_ListChanged никогда не вызывается, и я не понимаю почему.

Sebastian 15.05.2018 10:59

Я не вижу, чтобы вы поднимали какую-либо форму ChangeNotification внутри HouseList или даже от имени HouseList.

Christopher 15.05.2018 13:35

Я думаю, что ваш фиктивный код - большая часть проблемы. Вы создаете примерный список в средстве получения свойств. Мало того, что это не вызовет уведомления об изменении, разные вызывающие запросы получат даже разные экземпляры списка. Вы должны превратить его в правильную пару get / set. Возможно частный набор. А затем создайте и назначьте пример в другом коде. Функция «Загрузить», если она у вас есть. Или конструктор. Пока они работают со свойством, а не с резервным полем, уведомление об изменении позаботится обо всем остальном.

Christopher 15.05.2018 14:02

Я также не уверен, является ли BindingList правильным типом. На самом деле существует как минимум два варианта привязки - тот, который используется в графических интерфейсах MVVM / Dekstop. И тот, который используется в ASP.Net. У них нет ничего общего, кроме названия. Обычно мы используем ObservableCollection [T], а все остальные уведомления об изменениях оставляем Свойству, которое их раскрывает, или элементам, содержащимся в них. Я написал введение в MVVM несколько лет назад, исходя из моих собственных трудностей с его изучением. Это все еще может вам помочь: social.msdn.microsoft.com/Forums/vstudio/en-US/…

Christopher 15.05.2018 15:30

Я нашел решение, хотя не уверен, что это «правильный способ»:

В соответствии с рекомендациями я превратил AvgPricePerRoom в полноценное свойство с резервным полем и добавил функцию, которая устанавливает AvgPricePerRoom:

void CalculateAverage()
{
    AvgPricePerRoom =
          _selectedStreet.HouseList[0].Price / _selectedStreet.HouseList[0].Rooms / 3 +
          _selectedStreet.HouseList[1].Price / _selectedStreet.HouseList[1].Rooms / 3 +
          _selectedStreet.HouseList[2].Price / _selectedStreet.HouseList[2].Rooms / 3;
}

Затем я изменил свойство SelectedStreet на:

public StreetViewModel SelectedStreet
{
    get { return _selectedStreet; }
    set
    {
        _selectedStreet = value.Clone() as StreetViewModel;
        CalculateAverage();
        _selectedStreet.HouseList.ListChanged += (o, e) => CalculateAverage();
        RaisePropertyChanged();
    }
}

Это подходящее решение MVVM?

Я бы поставил CalculateAverage () после ChangeNotifciation. В противном случае обновления могут происходить в очень странном и противоречивом порядке. Единственный другой способ сделать это - зарегистрировать StreetViewModel в собственном PropertyChangeNotificaiton. Но на самом деле это всего лишь изменение варианта. Использование такого кода - это именно то, для чего были разработаны функции Properties или Get / Set. Это и наследование.

Christopher 17.05.2018 00:38

Другие вопросы по теме