Паттерн 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 шт):

Автор решения: aepot

Здесь всё просто. 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  - публичное свойство

Так код будет легче читать.

→ Ссылка