Разбирался с 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 слой (если нужен будет) и др. мелочи.
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Не могу перетащить файл из окна Solution Explorer в любую папку, например, на рабочий столMust have фича, ранее пользовался продуктом от JetBrains - Rider
Продолжаю разбираться с анимацией, и на этот раз не могу справиться с задачей плавной отрисовки круга, те
Как в Laravel средствами внутренних функций написать этот код более элегантнее?