1

In my datagrid, I have a textbox column and another column that should contain combination of combo box and text box which should be set dynamically. For instance, I'm letting user to set the status of the machine. So, State and Value are the headers of each column where Value can contain a comboBox or TextBox depending on type of the State. Where its type can be Boolean or enum. If its enum, then display combo box else textBox.

I'm trying to do this via view model and I'm not sure how to set the DataGridview in the xaml. Or is it possible with this scenario...?

<DataGrid Name="dataGridView" ItemsSource="{Binding Path=StateParametersList}" CanUserAddRows="False" 
                      IsReadOnly="True" >

                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding State}"/>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <ComboBox ItemsSource="{Binding ValueCell}" SelectedItem="{Binding Value}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>

            </DataGrid>

viewModel:

private ObservableCollection<StateParameters> StateParametersList =
        new ObservableCollection<StateParameters>();

    public ObservableCollection<StateParameters> StateParametersList
    {
        get { return StateParametersList; }
        set
        {
            StateParametersList = value;
            NotifyPropertyChanged(nameof(StateParametersList));
        }
    }
[Serializable]
public class StateParameters
{
    public string State { get; set; }
    public object Value { get; set; }
}

List<string> ValueCell = new List<string>();

where ValueCell will be list of items in the comboBox that will be filled at runtime.

So, I could have done this via xaml.cs file and created combo box depending on if its enum or not but I want to acheive this through view Model. And, Each comboBox will have different values which gets filled dynamiccaly at run time. I'm struggling here, hence, if anyone can point me to the right direction, will greatly appreciate it.

jamilia
  • 359
  • 4
  • 14
  • Why do you want a TextBox representing a boolean parameter? Wouldn't a CheckBox be a better choice for representing a boolean parameter? –  Jun 23 '19 at 12:06
  • Yes, check box would be ideal too. But how do I have a check box and a combo box in combination in datagrid which is filled dynamically? – jamilia Jun 23 '19 at 19:59
  • I already have an idea of how to address your issue, but allow me one more question to get a better handle on a possible solution: As you already noticed, your simple StateParamters data model is not really helping the needs of your application. You have decorated your StateParameters with a `[Serialize]` attribute, which looks to me that it will be used during (de)serialization. (1/2) –  Jun 23 '19 at 20:10
  • (2/2) Would it be acceptable for you to make StateParameters an interface or abstract class? And then additional classes derived from StateParameters, with each of those classes representing a different category of parameters (like toggable/bool, selection-based/enum, text, whatever...)? The motiviation behind this is to have a data model that better fits the needs of your application (interaction logic) –  Jun 23 '19 at 20:11
  • (Side note: In any case i suggest to rename the class `StateParameters` to `StateParameter`. A `StateParameters` instance only represents a single state parameter, so it is kinda wrong and misleading to use plural _StateParameters_ as the name of this class... ;-) ) –  Jun 23 '19 at 20:13
  • Thanks @elgonzo. Agree, with you on the naming of the class. It should have been just StateParameter. On (1/), yes, i would like to save and load it from the disk hence need to serialize/deserialize them. On your (2/2), i have created it as class only to allow me to use in ObservableCollection which I thought will help me bind to the datagrid view but since I'm unable to achieve it I'm open to modify the StateParameter(s) class if it allows me to achieve what I want. – jamilia Jun 23 '19 at 21:07
  • So, if i understand your last comment correctly, it is okay to expand your StateParameter data model a little bit, yes? –  Jun 23 '19 at 21:10
  • Yes, that is right! – jamilia Jun 23 '19 at 21:45

1 Answers1

1

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:

enter image description here

  • 1
    I tried this and this works like a charm and that was an awesome explanation. I kept struggling thinking there should be a straight forward way to handle it but looks like this is the way to go. Thanks so much for your time and effort. Highly appreciate! – jamilia Jun 24 '19 at 04:14