1. Organizing the state parameter data model
When looking at the desired user interactions, different categories of state parameters exits with respect of how they are to be presented/edited to/by the user. Within the scope of the question, we can identify the following categories:
- A toggeable parameter (
bool
)
- A choice parameter, where the value of the parameter is one out of a given set (like an enum, or any other data type, really)
- And for good measure, a text parameter (
string
)
2. Implementing the state parameter data model
A state parameter has a state name/identifier and a value. The value can be of varying type. This is essentially the definition of the StateParameters class in the question.
However, as will become more obvious later in my answer, having different types/classes representing the different categories of state parameters as listed above will be beneficial for wiring up the presentation and interaction logic in the UI.
Of course, no matter its category, each state parameter should be represented by the same base type. The obvious choice is to make the state parameter base type an abstract class or interface. Here, i opted for an interface:
public interface IStateParameter
{
string State { get; }
object Value { get; set; }
}
Instead of now directly creating the concrete state parameter classes according to the categories listed above, i create an additional abstract base class. This class will be generic, making the handling of state parameters in a type-safe way somewhat easier:
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; }
}
}
(While the State
property has a setter, this property is meant to be set only once, therefore property change notifications should not be necessary for it. Technically you could change the property anytime; i just chose to use a setter here to keep the code in my answer relatively short and simple.)
Note the implementation of the INotifyPropertyChanged
interface, which is necessary since the UI is going to manipulate the Value
property through bindings. Also note the explicit interface implementation of the IStateParameter
interface property Value
, which will "hide" it unless you explicitly cast a state parameter object reference as IStateParameter
. This is intentional, since StateParameter<T>
provides its own Value
property of a type that matches StateParameter<T>'s generic type parameter. Also, the equality comparison in the Value
setter is unfortunately somewhat akward, because the generic type parameter T
here is entirely unconstrained and could be either some value type or some reference type. Thus, the equality comparison has to cover all eventualities.
So, with these preparations done, it's time to direct our focus back onto the actual problem. We are now going to implement the concrete state parameter types according to the categories outlined at the beginning of the answer:
public class BoolStateParameter : StateParameter<bool>
{ }
public class TextStateParameter : StateParameter<string>
{ }
public class ChoiceStateParameter : StateParameter<object>
{
public Array Choices { get; set; }
}
The ChoiceStateParameter class declares an additional property which is used to hold the array with the possible values to choose from for a particular state parameter. (Like the StateParameter<T>.State above, this property is meant to be set only once, and the reason i gave it a setter here is to keep the code in my answer relatively short and simple.)
Aside from the ChoiceStateParameter class, no other class has any declaration in it. Why would we need BoolStateParameter/TextStateParameter if we could use StateParameter<bool>/StateParameter<string> directly, you ask? That's a good question. If we wouldn't have to deal with XAML, we could easily use StateParameter<bool>/StateParameter<string> directly (assuming _StateParameter<T> was not an abstract class). However, attempting to refer to generic types from within XAML markup is something between quite painful and outright impossible. Thus, the non-generic concrete state parameter classes BoolStateParameter, TextStateParameter and ChoiceStateParameter have been defined.
Oh, and before we forget, since we have declared the common state parameter base type as an interface named IStateParameter
, the type parameter of the StateParametersList
property in the viewmodel has to be adjusted accordingly (and its backing field, too, of course):
public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }
With this done, we have finished the part on the C# code side, and we move on to the DataGrid.
3. UI / XAML
Since the different state parameter categories demand different interaction elements (CheckBoxes, TextBoxes, ComboBoxes), we will attempt to leverage DataTemplates to define how each of those state parameter categories should be represented inside the DataGrid cells.
Now it will also become obvious why we made the effort to define those categories and declared different state parameter types for each of them. Because DataTemplates can be assosiacted with a specific type. And we are now going to define those DataTemplates for each the BoolStateParameter
, TextStateParameter
and ChoiceStateParameter
type.
The DataTemplates will be placed within the DataGrid, as part of the DataGrid's resource dictionary:
<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>
(Note: You might need to adapt the local:
namespace i used here or exchange it with a XML namespace that is mapped to the C# namespace in which you declare the state parameter classes.)
Next step is to make the DataGridTemplateColumn pick the appropriate DataTemplate depending on the actual type of state parameter it is dealing with in a given column cell. However, DataGridTemplateColumn cannot pick a DataTemplate from the resource dicationary itself, nor does the DataGrid control does it on behalf of DataGridTemplateColumn. So, what now?
Fortunately, there are UI elements in WPF which present some value/object using a DataTemplate from a resource dictionary, with the DataTemplate being choosen based on the type of the value/object. One such a UI element is ContentPresenter
, which we will use in the DataGridTemplateColumn:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding State}"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
And that's it. With a small expansion of the underlying data model (the state parameter classes), the XAML problems simply vanished (or so i hope).
4. Demo dataset
A quick test dataset to demonstrate the code in action (using randomly picked enum types as examples):
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"
}
};
It will look like this:
