Паттерн MVVM. Как обратиться к объекту модели
Есть программа. Она из себя представляет таблицу (DataGrid) в которой мы можем удалять/добавлять строки, а так же менять значения в столбцах/строках (их 8).
Код пытаюсь строить с использованием паттерна MVVM. В коде есть 3 класса:
- Line:INotifyPropertyChanged - представляет из себя "шаблон" строки, обрабатываем изменения параметров в столбцах. Объекты этого класса хранятся в коллекции класса M1Model;
public class Line: INotifyPropertyChanged
{
private int? _number;
private string? _type;
private float? _alpha;
private float? _height;
private float? _radius;
private float? _tickness;
private float? _refraction;
private float? _opticalForce;
public int? number { get => _number; set { _number = value; OnPropertyChanged(); } }
public string? type { get => _type; set { _type = value; OnPropertyChanged(); } }
public float? alpha { get => _alpha; set { _alpha = value; OnPropertyChanged(); } }
public float? height { get => _height; set { _height = value; OnPropertyChanged(); } }
public float? radius { get => _radius; set { _radius = value; OnPropertyChanged(); } }
public float? tickness { get => _tickness; set { _tickness = value; OnPropertyChanged(); } }
public float? refraction { get => _refraction; set { _refraction = value; OnPropertyChanged(); } }
public float? opticalForce { get => _opticalForce; set { _opticalForce = value; OnPropertyChanged(); } }
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
- M1Model:BindableBase - хранит в себе коллекцию из строк, команды для добавления/удаления строк и методы для математических операций со значениями в строках/столбцах;
public class M1Model : BindableBase
{
public readonly ReadOnlyObservableCollection<Line> lines;
public M1Model()
{
lines = new ReadOnlyObservableCollection<Line>(_lines);
}
private ObservableCollection<Line> _lines = new ObservableCollection<Line>()
{
new Line() { number = 0 }
};
public void AddRow(int index)
{
_lines.Insert(index, new Line());
for (int i = 0; i < _lines.Count; i++)
{
_lines[i].number = i;
}
}
public void RemoveRow(int index)
{
if (_lines.Count > 1)
{
_lines.RemoveAt(index);
for (int i = 0; i < _lines.Count; i++)
{
_lines[i].number = i;
}
}
}
public void UpdateAll()
{
for (int i = 0; i < _lines.Count; i++)
{
Alpha();
Height();
}
}
public void Alpha()
{
for (int i = 2; i < _lines.Count(); i+=2)
{
_lines[i].alpha = _lines[i - 2].alpha + _lines[i - 2].height * _lines[i - 2].opticalForce;
}
for (int i = 1; i < _lines.Count(); i+=2)
{
_lines[i].alpha = (_lines[i - 1].alpha + _lines[i + 1].alpha) / 2;
}
}
public void Height()
{
for (int i = 2; i < _lines.Count(); i+=2)
{
_lines[i].height = _lines[i - 2].height - _lines[i].alpha * _lines[i - 1].tickness;
}
for (int i = 1; i < _lines.Count(); i += 2)
{
_lines[i].height = _lines[i - 1].height;
}
}
}
- M1VM:BindableBase - хранит ссылку на модель и команды;
public class M1VM : BindableBase
{
readonly public M1Model _model = new M1Model();
public ReadOnlyObservableCollection<Line> allLines => _model.lines;
public DelegateCommand<int?> AddRow { get; }
public DelegateCommand<int?> RemoveRow { get; }
public M1VM()
{
AddRow = new DelegateCommand<int?>(i => { if (i.HasValue) _model.AddRow(i.Value); });
RemoveRow = new DelegateCommand<int?>(i => { if (i.HasValue) _model.RemoveRow(i.Value); });
}
}
- Вьюшка (Control).
<UserControl ...>
<UserControl.DataContext>
<local:M1VM/>
</UserControl.DataContext>
<Grid >
<DataGrid x:Name="dataGrid" ItemsSource="{Binding allLines}" AutoGenerateColumns="False" CellEditEnding="dataGrid_CellEditEnding">
<DataGrid.InputBindings>
<KeyBinding Key="Insert" Command="{Binding AddRow}" CommandParameter="{Binding ElementName=dataGrid, Path=SelectedIndex}"/>
<KeyBinding Key="Delete" Command="{Binding RemoveRow}" CommandParameter="{Binding ElementName=dataGrid, Path=SelectedIndex}"/>
</DataGrid.InputBindings>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding number, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Header="№" Width="40" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding type}" IsReadOnly="True" Header="Тип поверхности" Width="110" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding alpha, Mode=TwoWay}" Header="Угол луча (a)" Width="80" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding height, Mode=TwoWay}" Header="Высота луча (h)" Width="100" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding radius, Mode=TwoWay}" Header="Радиус (r)" Width="140" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding tickness, Mode=TwoWay}" Header="Толщина" Width="100" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding refraction, Mode=TwoWay}" Header="Показатель преломления (n)" Width="150" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
<DataGridTextColumn Binding="{Binding opticalForce, Mode=TwoWay}" Header="Оптическаяя сила (F)" Width="150" TextBlock.TextAlignment="Center" TextBlock.FontSize="16"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
Возникла следующая проблема: после завершения "редактирования" ячейки, мне нужно провести математические операции во всей таблице (во всех строках). Узнать об изменении в ячейке я могу в классе Line. Но ссылка на модель хранится во VM. Каким образом можно уведомить M1VM или M1Model об изменении в Line? Или как можно обратиться из объекта Line к коллекции, в которой он хранится?
Ответы (1 шт):
Здесь всё просто. ObservableCollection имеет событие CollectionChanged. Подписавшись на него можно видеть события добавления и удаления элементов.
Каждый элемент коллекции имеет событие PropertyChanged, которое вы сами же вызываете в сеттерах свойств, то есть при их изменении. Именно отслеживая эти события вью узнаёт об изменениях в данных.
Можно написать вот такой обработчик:
private void OnLineChanged(object sender, PropertyChangedEventArgs e)
{
Line line = (Line)sender;
object value = typeof(Line).GetProperty(e.PropertyName).GetValue(line);
Debug.WriteLine($"Line property changed: {e.PropertyName} = {value}");
}
Он просто покажет, какое свойство было изменено и с помощью рефлексии получит его значение.
Теперь чтобы при добавлении в коллекцию подписываться, а при удалении отписываться от изменений в Line, потребуется такой обработчик:
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems?.Count > 0)
{
foreach (var item in e.NewItems.Cast<INotifyPropertyChanged>())
item.PropertyChanged += OnLineChanged;
Debug.WriteLine($"Lines added: {e.NewItems.Count}");
}
if (e.OldItems?.Count > 0)
{
foreach (var item in e.OldItems.Cast<INotifyPropertyChanged>())
items.PropertyChanged -= OnLineChanged;
Debug.WriteLine($"Lines removed: {e.NewItems.Count}");
}
}
Теперь прикрутить это добро к коллекции. Для начала нужно убрать создание нового элемента при объявлении коллекции.
private ObservableCollection<Line> _lines = new ObservableCollection<Line>();
Теперь в конструкторе подписаться и добавить убранный выше элемент.
_lines.CollectionChanged += OnCollectionChanged;
_lines.Add(new Line() { number = 0 });
В противном случае он не отслеживался бы обработчиками.
Вот, собственно и всё. Вы ослеживаете любое изменение любого поля в коллекции, а также добавление и удаление элементов.
System.Diagnostics.Debug.WriteLine выводит текст в консоль студии во время отладки приложения.
Теперь подсказки. BindableBase это и есть реализация INotifyPropertyChanged (из библиотеки Prism MVVM, верно?), то есть реализовать вручную смысла нет, получится так, приведу фрагмент:
public class Line : BindableBase
{
private int? _number;
// ... остальные поля
public int? number { get => _number; set => SetProperty(ref _number, value); }
// ... остальные свойства
}
Всё, больше ничего не надо, событие PropertyChanged и метод OnPropertyChanged убрать.
И последнее. Поля и переменные пишутся с маленькой буквы, а классы, свойства и методы - с большой буквы. А у вас свойства с маленькой, поправьте.
Получится:
number - локальная переменная или аргумент метода
_number - приватное поле
Number - публичное свойство
Так код будет легче читать.