Search code examples
c#wpfcomboboxdatagrid

ValidationRule : Get selected combobox row number within datagrid WPF C#


I created a datagrid where each row represent a command for a machine in our lab. I want to restrain the user's option to select "Relative" in the "Limit Mode" column by implementing validation rules within my code. The user can use a "Relative" limit mode only if the move control associated to this row AND the previous are the same (see picture). Otherwise a message should warn him.

validation rule principle

I manage to create a RowValidationRules that works:

XAML:

<DataGrid.RowValidationRules>
    <utility:RelativeLimitModeRule ValidationStep="UpdatedValue"/>
</DataGrid.RowValidationRules>

Validation Rule:

class RelativeLimitModeRule : ValidationRule
{
    //Makes sure that if "Relative" move position is selected, the previous command had the same MoveCtrl (otherwise relative is bullshit)
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value is BindingGroup bg && bg.Items.Count > 0)
        {
            if ((bg.Items[0] as DoliInput).LimMode == "Relative")
            {
                if (bg.Owner is DataGridRow dgrow)
                {
                    DataGrid dg = GetParent<DataGrid>(dgrow);
                    int currItemKey = ((bg.Items[0]) as DoliInput).SequenceNumber - 1;
                    if (currItemKey > 0)
                    {
                        if (((((InteractiveGraph.MVVM.ViewModel)dg.DataContext).DoliInputCollection)[currItemKey - 1]).MoveCtrl != (bg.Items[0] as DoliInput).MoveCtrl)
                        {
                            return new ValidationResult(false, $"To use the \"Relative\" option, the previous command should have the same Move Ctrl as this one ");
                        }
                    }
                }
            }
        }
        return ValidationResult.ValidResult;
        //return new ValidationResult(true, null);
    }
    private static T GetParent<T>(DependencyObject d) where T : class
    {
        while (d != null && !(d is T))
        {
            d = VisualTreeHelper.GetParent(d);
        }
        return d as T;

    }
}

Where DoliInput is my model, with four different properties: MoveCtrl,Speed,LimitMode and a last one not visible SequenceNumber that keeps track of the position of the command within DoliInputCollection which is an ObservableCollection that stores/lists all the command to be processed by the machine. However, when implementing it this way, the validation rule only trigger when the user selects another row AND modifies the "Spe" column.

I modified the XAML so the Validation rule fires as soon as the user selects "Relative" as limit mode:

New Xaml:

<DataGridTemplateColumn Header="Limit Mode">
<DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
        <ComboBox ItemsSource="{Binding LimModeItem}">
            <ComboBox.SelectedItem>
                <Binding Path="LimMode"
                         UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <utility:RelativeLimitModeRule>
                        </utility:RelativeLimitModeRule>
                    </Binding.ValidationRules>
                </Binding>
            </ComboBox.SelectedItem>
        </ComboBox>
    </DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <ComboBox ItemsSource="{Binding LimModeItem}" 
                  SelectedItem="{Binding LimMode, UpdateSourceTrigger=PropertyChanged}" />
    </DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>

But now the value parameter in the validation rule returns the select string, and I don't have access to the row index which I need for my comparison between the current row Move Control and the previous one.

Using a the Combobox_DropDownOpened event, I am able to retrieve the row index.

private void ComboBox_DropDownOpened(object sender, EventArgs e)
{
    var cb = ((System.Windows.Controls.ComboBox)sender);
    DataGridRow dataGridRow = VisualHelper.FindParent<DataGridRow>(cb);
    int index = dataGridRow.GetIndex();
}

But I want to keep my CodeBehind clear. So I tried using Interactivity to bind a command to the DropDownEvent, but I don't know how to work on the sender like I would do using the event.

Am I going the right way ? If so, how could I do to retrieve the row number using a command ? Is there a better to setup the validation rule ?


Solution

  • Alright, ValidationStep="UpdatedValue" not only defines when the Validation rule is fired, but it also has an impact on the value sent to the validationrule.

    By going from

    <DataTemplate>
        <ComboBox ItemsSource = "{Binding LimModeItem}" >
            < ComboBox.SelectedItem >
                < Binding Path="LimMode"
                            UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <utility:RelativeLimitModeRule/>
                    </Binding.ValidationRules>
                </Binding>
            </ComboBox.SelectedItem>
        </ComboBox>
    </DataTemplate>
    

    To

    <DataTemplate>
        <ComboBox ItemsSource = "{Binding LimModeItem}" >
            < ComboBox.SelectedItem >
                < Binding Path="LimMode"
                            UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <utility:RelativeLimitModeRule ValidationStep="UpdatedValue"/ >
                    </Binding.ValidationRules>
                </Binding>
            </ComboBox.SelectedItem>
        </ComboBox>
    </DataTemplate>
    

    The type of the valueparameter went from string to BindingExpression. From there, I can work my way to the row index.