1.Организация модели данных параметров состояния
При рассмотрении желаемых пользовательских взаимодействий существуют различные категории параметров состояния в отношении того, как они должны быть представлены / отредактированы пользователем.В рамках вопроса мы можем выделить следующие категории:
- Переключаемый параметр (
bool
) - Параметр выбора, где значение параметра равно единице.заданного набора (например, перечисления или любого другого типа данных)
- И, если быть точным, текстовый параметр (
string
)
2.Реализация модели данных параметра состояния
Параметр состояния имеет имя / идентификатор состояния и значение.Значение может быть различного типа.По сути, это определение класса StateParameters в вопросе.
Однако, как станет более очевидным позже в моем ответе, различные типы / классы представляют различные категории параметров состояния какперечисленные выше будут полезны для подключения логики представления и взаимодействия в пользовательском интерфейсе.
Конечно, независимо от его категории, каждый параметр состояния должен быть представлен одним и тем же базовым типом.Очевидный выбор - сделать базовый тип параметров состояния абстрактным классом или интерфейсом.Здесь я выбрал интерфейс:
public interface IStateParameter
{
string State { get; }
object Value { get; set; }
}
Вместо непосредственного создания классов параметров конкретного состояния в соответствии с категориями, перечисленными выше, я создаю дополнительный абстрактный базовый класс.Этот класс будет универсальным, что облегчит обработку параметров состояния безопасным для типов способом:
public abstract class StateParameter<T> : IStateParameter, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string State { get; set; }
public T Value
{
get { return _v; }
set
{
if ((_v as IEquatable<T>)?.Equals(value) == true || ReferenceEquals(_v, value) || _v?.Equals(value) == true)
return;
_v = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
private T _v;
object IStateParameter.Value
{
get { return this.Value; }
set { this.Value = (T) value; }
}
}
(Хотя свойство State
имеет установщик, это свойство должно устанавливаться только один разпоэтому уведомления об изменении свойства не должны быть необходимы для него. Технически вы можете изменить свойство в любое время; я просто решил использовать установщик, чтобы код в моем ответе был относительно коротким и простым.интерфейса INotifyPropertyChanged
, что необходимо, поскольку пользовательский интерфейс будет манипулировать свойством Value
через привязки.Также обратите внимание на явную реализацию интерфейса свойства интерфейса IStateParameter
Value
, которое будет "скрывать" его, если вы явно не приведете ссылку на объект параметра состояния как IStateParameter
.Это сделано намеренно, поскольку StateParameter<T>
предоставляет собственное свойство Value
типа, соответствующего параметру универсального типа StateParameter .Кроме того, сравнение на равенство в установщике Value
, к сожалению, несколько неудобно, потому что параметр универсального типа T
здесь совершенно не ограничен и может быть либо некоторым типом значения, либо некоторым ссылочным типом.Таким образом, сравнение на равенство должно охватывать все возможные варианты.
Итак, после этих приготовлений пришло время снова сосредоточиться на актуальной проблеме.Теперь мы собираемся реализовать конкретные типы параметров состояния в соответствии с категориями, указанными в начале ответа:
public class BoolStateParameter : StateParameter<bool>
{ }
public class TextStateParameter : StateParameter<string>
{ }
public class ChoiceStateParameter : StateParameter<object>
{
public Array Choices { get; set; }
}
Класс ChoiceStateParameter объявляет дополнительныйсвойство, которое используется для хранения массива с возможными значениями на выбор для определенного параметра состояния.(Как и StateParameter .State выше, это свойство должно устанавливаться только один раз, и причина, по которой я дал ему установщик, заключается в том, чтобы код в моем ответе был относительно коротким и простым.)
Кроме ChoiceStateParameter класса, ни у одного другого класса нет никакого объявления в нем.Вы спросите, зачем нам BoolStateParameter / TextStateParameter , если бы мы могли напрямую использовать StateParameter / StateParameter ?Это хороший вопрос.Если бы нам не пришлось иметь дело с XAML, мы могли бы легко использовать StateParameter / StateParameter напрямую (при условии, что _StateParameter не был абстрактным классом).Тем не менее, попытка ссылаться на универсальные типы из разметки XAML является чем-то между довольно болезненным и совершенно невозможным.Таким образом, неуниверсальные классы параметров конкретного состояния BoolStateParameter , TextStateParameter и ChoiceStateParameter были определены.
О, и прежде чем мы забудем,поскольку мы объявили базовый тип параметра общего состояния в качестве интерфейса с именем IStateParameter
, параметр типа свойства StateParametersList
в viewmodel должен быть соответствующим образом скорректирован (и, конечно, и его вспомогательное поле):
public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }
После этого мы завершили часть на стороне кода C # и переходим к DataGrid.
3.UI / XAML
Поскольку различные категории параметров состояния требуют разных элементов взаимодействия (CheckBoxes, TextBoxes, ComboBoxes), мы попытаемся использовать DataTemplates, чтобы определить, как каждая из этих категорий параметров состояния должна быть представлена внутриЯчейки DataGrid.
Теперь также станет понятно, почему мы предприняли попытку определить эти категории и объявили разные типы параметров состояния для каждой из них.Потому что DataTemplates могут быть связаны с определенным типом.И теперь мы собираемся определить эти DataTemplates для каждого типа BoolStateParameter
, TextStateParameter
и ChoiceStateParameter
.
DataTemplates будут размещены в DataGrid, как часть словаря ресурсов DataGrid:
<DataGrid Name="dataGridView" ItemsSource="{Binding Path=StateParametersList}" ... >
<DataGrid.Resources>
<DataTemplate DataType="{x:Type local:BoolStateParameter}">
<CheckBox IsChecked="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:TextStateParameter}">
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ChoiceStateParameter}">
<ComboBox ItemsSource="{Binding Choices}" SelectedItem="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGrid.Resources>
( Примечание. Возможно, вам придется адаптировать используемое здесь пространство имен local:
или заменить его пространством имен XML, которое сопоставлено с пространством имен C #, в котором вы объявляете классы параметров состояния. )
Следующий шаг - заставить DataGridTemplateColumn выбрать соответствующий DataTemplate в зависимости от фактического типа параметра состояния, с которым он имеет дело в данной ячейке столбца.Однако DataGridTemplateColumn не может выбрать DataTemplate из самого словаря ресурсов, а элемент управления DataGrid не делает этого от имени DataGridTemplateColumn .Итак, что теперь?
К счастью, в WPF есть элементы пользовательского интерфейса, которые представляют некоторое значение / объект, используя DataTemplate из словаря ресурсов, причем DataTemplate выбирается на основе типа значения / объекта.Одним из таких элементов пользовательского интерфейса является ContentPresenter
, который мы будем использовать в DataGridTemplateColumn :
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding State}"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
И это все.С небольшим расширением базовой модели данных (классы параметров состояния) проблемы XAML просто исчезли (или я так надеюсь).
4.Набор демонстрационных данных
Быстрый набор тестовых данных для демонстрации кода в действии (с использованием случайно выбранных типов перечислений в качестве примеров):
StateParametersList = new ObservableCollection<IStateParameter>
{
new BoolStateParameter
{
State = "Bool1",
Value = false
},
new ChoiceStateParameter
{
State = "Enum FileShare",
Value = System.IO.FileShare.ReadWrite,
Choices = Enum.GetValues(typeof(System.IO.FileShare))
},
new TextStateParameter
{
State = "Text1",
Value = "Hello"
},
new BoolStateParameter
{
State = "Bool2",
Value = true
},
new ChoiceStateParameter
{
State = "Enum ConsoleKey",
Value = System.ConsoleKey.Backspace,
Choices = Enum.GetValues(typeof(System.ConsoleKey))
},
new TextStateParameter
{
State = "Text2",
Value = "World"
}
};
Это будет выглядеть так: