Технология связки в WPF

118
01 декабря 2019, 06:30

Разбирался с binding в WPF и столкнулся с проблемой. У меня есть класс "Sensor", окно, в котором расположен только "DataGrid".

У "DataGrid" есть 3 столбца: "название элемента", "состояние датчика", "значение датчика", которые соответственно связаны со свойствами Name, Condition, Value и в добавок свойство второго выпадающего списка IsEnabled связано со свойством IsActived у класса "Sensor". Проблема в том, что при изменении свойств элементов в списке, не меняются свойства, которые я связал. То есть мне приходится сбрасывать SourceItems в null и потом снова присваивать ему мой список source (можно найти в методе ComboBox_SelectionChanged в коде "MainWindow.xaml.cs"). По логике, если я выбираю в первом выпадающем списке "Выключен", то свойство Sensor.IsActivated становится равным false и следовательно, связанное с ним свойство второго выпадающего списка Combobox.IsEnabled должно также равняться false, однако я по прежнему могу взаимодействовать с этим элементом управления. Пожалуйста помогите решить данную проблему.

Простой класс Sensor здесь:

public sealed class Sensor
{
    public string Name { get; set; }
    public byte Condition { get; set; }
    public byte Value { get; set; }
    public bool IsActivated => Condition == 0
    public Sensor(string name, byte condition, byte value)
    {
        Name = name;
        Value = value;
        Condition = condition;
    }
}

Дизайн окна и сами связки здесь:

<Window x:Class="Sample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MainWindow"
    Height="450"
    Width="800">
<!--Сама таблица-->
<DataGrid Name="Table"
          Margin="10"
          LoadingRow="Table_LoadingRow"
          GridLinesVisibility="None"
          AutoGenerateColumns="False">
    <DataGrid.Columns>
        <!--Просто левая строчка-->
        <DataGridTemplateColumn Header="Название элемента">
            <DataGridTemplateColumn.CellTemplate>
                <ItemContainerTemplate>
                    <TextBlock Text="{Binding Path=Name}"
                               Margin="5" />
                </ItemContainerTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <!--Если в этом выпадающем списке будет "выключен", то второй станет не доступен-->
        <DataGridTemplateColumn Header="Состояние датчика">
            <DataGridTemplateColumn.CellTemplate>
                <ItemContainerTemplate>
                    <ComboBox SelectedIndex="{Binding Path=Condition}"
                              SelectionChanged="ComboBox_SelectionChanged">
                        <ComboBoxItem Content="Включен" />
                        <ComboBoxItem Content="Выключен" />
                    </ComboBox>
                </ItemContainerTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <!--Второй выпадающий список-->
        <DataGridTemplateColumn Header="Значение датчика">
            <DataGridTemplateColumn.CellTemplate>
                <ItemContainerTemplate>
                    <ComboBox SelectedIndex="{Binding Path=Value}"
                              IsEnabled="{Binding Path=IsActivated}">
                        <ComboBoxItem Content="Не доступно" />
                        <ComboBoxItem Content="Открыто" />
                        <ComboBoxItem Content="Закрыто" />
                    </ComboBox>
                </ItemContainerTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Код из файла MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    List<Sensor> source = new List<Sensor> {
        new Sensor("Дверь в гостиную", 0, 1),
        new Sensor("Дверь в кухню", 1, 0),
        new Sensor("Входая дверь", 0, 1),
        new Sensor("Правое окно балкона", 1, 0),
        new Sensor("Левое окно балкона", 0, 1),
        new Sensor("Окно в гостинной", 0, 2)
    };
    public MainWindow()
    {
        InitializeComponent();
        Table.ItemsSource = source;
    }
    void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (Table.SelectedIndex != -1) {
            var condition = (byte)(sender as ComboBox).SelectedIndex;
            source[Table.SelectedIndex].Condition = condition;
            Table.ItemsSource = null;
            Table.ItemsSource = source;
        }
    }
    private void Table_LoadingRow(object sender, DataGridRowEventArgs e)
    {
        var sensor = (Sensor)e.Row.DataContext;
        if (sensor.IsActivated) {
            e.Row.Background = Brushes.White;
        } else {
            e.Row.Background = Brushes.LightCoral;
        }
    }
}

Здесь вы сможете скачать архив с проектом.

Answer 1

И так, давайте по порядку...

DataContext и ItemsSource

Вы задаете this.Table.ItemsSource = source;, хорошо, но что если у вас будет 10, 20, 30 контролов? У вас будет портянка кода с указанием ItemsSource? Также вы должны стремится к отделению View (своего XAML) от ViewModel (вашего кода) и в таком случае указание ItemSource полностью должно быть реализовано в View части приложения. Для установки ItemSource из XAML нам надо указать соответствующий DataContext. Давайте сделаем это:

  • Вместо this.Table.ItemsSource = source; мы пишем DataContext = this;. this в данном случае, это MainWindow, но лучше создать отдельный класс (прим: MainViewModel) и его уже тут использовать.
  • Коллекцию source переделываем в свойство и делаем публичным. - public List<Sensor> source { get; set; } = .... Если вы будете добавлять/удалять объекты из коллекции, то стоит использовать ObservableCollection<T>.
  • Убираем Name="Table" в XAML. Отвыкайте от работы с контролами через код, привязки ваше все!
  • Задаем DataGrid нужный ItemSource - ItemsSource="{Binding source}"

Все, теперь у вас более менее правильная привязка с которой мы можем работать дальше.

INotifyPropertyChanged

Если мы привязали какое то свойство и меняем его через код, то для обновления View части нам необходимо реализовать INotifyPropertyChanged:

  • Создадим класс, назовем его к примеру BaseVM.
  • Наследуем его от INotifyPropertyChanged.
  • Реализовываем самым простейшим способом:

    public class BaseVM : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName]string prop = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
        }
    }
    
  • Наследуем от созданного нами класса тот класс, где содержатся изменяемые свойства (public sealed class Sensor : BaseVM).

  • В Set нужного свойства добавляем вызов INPC:

    private bool isActivated;
    public bool IsActivated
    {
        get => isActivated;
        set
        {
            isActivated = value;
            OnPropertyChanged();
        }
    }
    

Переносим логику

  1. Вашу старую логику public bool IsActivated => Condition == 0; мы переносим в свойство Condition (p.s. SelectedIndex это int):

    private int condition;
    public int Condition {
        get => condition;
        set
        {
            condition = value;
            IsActivated = value == 0;
        }
    }
    

    Скорей всего не будет обновляться свойство. Дописываем в XAML UpdateSourceTrigger в привязке ({Binding Path=Condition, UpdateSourceTrigger=PropertyChanged}).

    Все, теперь нам полностью не нужен ComboBox_SelectionChanged, отписываемся от него.

  2. Цвет строки. Любые изменения в UI - это View часть, туда и стоит перенести. Реализуется это довольно легко, путем написания триггера. Внутри DataGrid пишем:

    <DataGrid.RowStyle>
        <Style TargetType="DataGridRow">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsActivated}" Value="True">
                    <Setter Property="Background" Value="White"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding IsActivated}" Value="False">
                    <Setter Property="Background" Value="LightCoral"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </DataGrid.RowStyle>
    

    Ну и убираем соответственно Table_LoadingRow.

Все, теперь ваше приложение имеет правильную привязку, без лишних событий и все по своим местам.

реализовать данное приложение, как оно есть, по шаблону MVVM

Собственно почему бы и нет.
Для начала Вам стоит понимать, что такое MVVM подход и для чего он. Я особо рассказывать про его тонкости не буду, главное поймите то, что MVVM - это разделение логики приложения на 3 слоя, которые не связаны друг с другом почти не чем.

  • Model - источник данных. В вашем случае это могут быть сенсоры, которые например берутся из базы или от куда либо еще.
  • View - UI нашего приложения. В вашем случае это просто MainWindow.
  • ViewModel - некий связующий слой, который работает с Model и предоставляет для View публичные свойства, к которым та в последующем привязывается.

Теперь давайте перепишем ваше приложение (имеем уже те правки, что выше).

Основное:

  1. Создадим 3 папки, Models, ViewModels, Views (для удобства и лучшего понимания).
  2. В Views перекидываем MainWindows целиком. После перекидывания изменяем в .cs и .xaml namespace (добавляем .Views (Sample.Views..).
  3. В ViewModels переносим ранее созданный BaseVM (с заменой namespace).
  4. В ViewModels создаем главный класс MainViewModel.
  5. Теперь мы можем соединить все. Заходим в App.xaml и удаляем там StartupUri="MainWindow.xaml".
  6. Заходим в App.xaml.cs и переписываем событие OnStartup, делая что то вроде этого:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        new MainWindow() { DataContext = new MainViewModel() }.Show();
    }
    

    Так мы задали нужный нам DataContext и сами отображаем окно.

  7. Удаляем из MainWindow.xaml.cs указанный нами ранее DataContext.

  8. В местах где будет ругаться на namespace - дописываем нужный и запускаем проект. Должно успешно отобразится окно с дизайном, но без данных.

Убираем лишнее из View слоя:

  1. Убираем все имена из XAML, они нам не к чему, их стоит использовать лишь в стилях, каких то триггерах или в чем то еще, где работа чисто на XAML.
  2. Для удобства добавляем к Window (в XAML) следующее:

    xmlns:vm="clr-namespace:Sample.ViewModels"
    d:DataContext="{d:DesignInstance {x:Type vm:MainViewModel}}"
    

    Это позволит дизайнеру знать, какой DataContext сейчас установлен и он будет подсказывать нам об ошибках и предлагать свойства.

  3. Убираем все Path= (вкусовщина), я считаю их лишним мусором.

  4. Класс Sensor... Ну смотрите, это по сути источник данных и если сенсоры будут где то в базе или в другом месте, то стоит создать для них Model, где вы пропишете всю логику взаимодействия, пока же у вас это простой List<>, который спокойно можно уместить в VM слое. И по этому Переименовываем его в SensorViewModel, переносим в нужную папку, меняем namespace.
  5. В MainViewModel переносим инициализацию наших сенсоров в созданное публичное свойство коллекции:

    class MainViewModel
    {
        public ObservableCollection<SensorViewModel> Sensors { get; }
        public MainViewModel()
        {
            Sensors = new ObservableCollection<SensorViewModel>()
            {
                new SensorViewModel("Дверь в гостиную", 0, 1),
                new SensorViewModel("Дверь в кухню", 1, 0),
                new SensorViewModel("Входная дверь", 0, 1),
                new SensorViewModel("Правое окно балкона", 1, 0),
                new SensorViewModel("Левое окно балкона", 0, 1),
                new SensorViewModel("Окно в гостиной", 0, 2)
            };
        }
    }
    
  6. Привязываем DataGrid в XAML к этой коллекции (ItemsSource="{Binding Sensors}").

Все, теперь ваше приложение разделено на слои. В XAML окна мы не связаны какими либо событиями с кодом, в MainWindow.xaml.cs у нас только инициализация контролов, а в MainViewModel у нас нету не намека на контролы из View слоя, ибо идет работа с данными напрямую. Вам же остается доделать все под себя, сделать Model слой (если нужен будет) и др. мелочи.

READ ALSO
авторизация по ролям 403

авторизация по ролям 403

есть метод action:

127
Перетаскивание файлов Visual Studio

Перетаскивание файлов Visual Studio

Не могу перетащить файл из окна Solution Explorer в любую папку, например, на рабочий столMust have фича, ранее пользовался продуктом от JetBrains - Rider

113
WPF плавная отрисовка круга

WPF плавная отрисовка круга

Продолжаю разбираться с анимацией, и на этот раз не могу справиться с задачей плавной отрисовки круга, те

79
Как написать код элегантнее? Laravel

Как написать код элегантнее? Laravel

Как в Laravel средствами внутренних функций написать этот код более элегантнее?

120