Разбирался с 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;
}
}
}
Здесь вы сможете скачать архив с проектом.
И так, давайте по порядку...
Вы задаете 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}"Все, теперь у вас более менее правильная привязка с которой мы можем работать дальше.
Если мы привязали какое то свойство и меняем его через код, то для обновления 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();
}
}
Вашу старую логику 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, отписываемся от него.
Цвет строки. Любые изменения в 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 слоя, которые не связаны друг с другом почти не чем.
Теперь давайте перепишем ваше приложение (имеем уже те правки, что выше).
Основное:
Models, ViewModels, Views (для удобства и лучшего понимания).Views перекидываем MainWindows целиком. После перекидывания изменяем в .cs и .xaml namespace (добавляем .Views (Sample.Views..).ViewModels переносим ранее созданный BaseVM (с заменой namespace).ViewModels создаем главный класс MainViewModel.App.xaml и удаляем там StartupUri="MainWindow.xaml".Заходим в App.xaml.cs и переписываем событие OnStartup, делая что то вроде этого:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new MainWindow() { DataContext = new MainViewModel() }.Show();
}
Так мы задали нужный нам DataContext и сами отображаем окно.
Удаляем из MainWindow.xaml.cs указанный нами ранее DataContext.
Убираем лишнее из View слоя:
Для удобства добавляем к Window (в XAML) следующее:
xmlns:vm="clr-namespace:Sample.ViewModels"
d:DataContext="{d:DesignInstance {x:Type vm:MainViewModel}}"
Это позволит дизайнеру знать, какой DataContext сейчас установлен и он будет подсказывать нам об ошибках и предлагать свойства.
Убираем все Path= (вкусовщина), я считаю их лишним мусором.
Sensor... Ну смотрите, это по сути источник данных и если сенсоры будут где то в базе или в другом месте, то стоит создать для них Model, где вы пропишете всю логику взаимодействия, пока же у вас это простой List<>, который спокойно можно уместить в VM слое. И по этому Переименовываем его в SensorViewModel, переносим в нужную папку, меняем namespace.В 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)
};
}
}
Привязываем DataGrid в XAML к этой коллекции (ItemsSource="{Binding Sensors}").
Все, теперь ваше приложение разделено на слои. В XAML окна мы не связаны какими либо событиями с кодом, в MainWindow.xaml.cs у нас только инициализация контролов, а в MainViewModel у нас нету не намека на контролы из View слоя, ибо идет работа с данными напрямую. Вам же остается доделать все под себя, сделать Model слой (если нужен будет) и др. мелочи.
Продвижение своими сайтами как стратегия роста и независимости