Я новичок в 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 в этом примере?
(Не весь приведенный выше код важен для моей проблемы, и я сожалею об этом ...)
Еще один совет: обратите внимание, что свойство HouseList проверяет, есть ли _houseList==null, и если это так, оно пытается вызвать Add для нулевого объекта. Так что, если это действительно возможно, у вас появится исключение NullReferenceException.
спасибо @Richardissimo, я редактировал код





При связывании коллекций существует 3 вида уведомлений об изменениях, о которых необходимо позаботиться:
Мой код, вероятно, слишком вложен, но я сосредотачиваюсь на BindingList<HouseViewModel> HouseList, который представлен DataGrid - и именно на вашем пункте 3. Как TextBox с AvgPricePerRoom всегда будет показывать обновленный результат на основе каждого изменения в DataGrid?
После того, как у вас есть надлежащее уведомление об изменении, класс Binding позаботится о деталях. Все, что он действительно делает, это регистрирует необходимые события на обоих концах (ViewModel и View) и назначает изменение одному концу на другом (если TwoWayMode).
Также вы можете превратить AvgPricePerRoom в полноценное свойство с резервным полем. Сейчас это свойство только для получения с вычисленным возвращаемым значением, которое требует, чтобы вы вызывали его ChangeNotification из другого места. Вместо этого создайте функцию «CalculateAverage», которая вычисляет среднее значение и присваивает его свойству, а свойство повышает свое собственное уведомление об изменении.
Итак, я отредактировал код, и теперь мой HouseList должен отслеживать изменения в своих элементах через HouseList.ListChanged + = HouseList_ListChanged ;. Однако, когда элементы редактируются в сетке данных, обработчик HouseList_ListChanged никогда не вызывается, и я не понимаю почему.
Я не вижу, чтобы вы поднимали какую-либо форму ChangeNotification внутри HouseList или даже от имени HouseList.
Я думаю, что ваш фиктивный код - большая часть проблемы. Вы создаете примерный список в средстве получения свойств. Мало того, что это не вызовет уведомления об изменении, разные вызывающие запросы получат даже разные экземпляры списка. Вы должны превратить его в правильную пару get / set. Возможно частный набор. А затем создайте и назначьте пример в другом коде. Функция «Загрузить», если она у вас есть. Или конструктор. Пока они работают со свойством, а не с резервным полем, уведомление об изменении позаботится обо всем остальном.
Я также не уверен, является ли BindingList правильным типом. На самом деле существует как минимум два варианта привязки - тот, который используется в графических интерфейсах MVVM / Dekstop. И тот, который используется в ASP.Net. У них нет ничего общего, кроме названия. Обычно мы используем ObservableCollection [T], а все остальные уведомления об изменениях оставляем Свойству, которое их раскрывает, или элементам, содержащимся в них. Я написал введение в MVVM несколько лет назад, исходя из моих собственных трудностей с его изучением. Это все еще может вам помочь: social.msdn.microsoft.com/Forums/vstudio/en-US/…
Я нашел решение, хотя не уверен, что это «правильный способ»:
В соответствии с рекомендациями я превратил 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. Это и наследование.
Несвязанный совет: лучше
RaisePropertyChanged("AvgPricePerRoom");, чемRaisePropertyChanged(nameof(AvgPricePerRoom));. Это позволяет избежать наличия строки, представляющей имя свойства, и риска того, что одно может измениться без изменения другого.