6

At the moment I have a grid and I'm trying to have a cell with validation rules. To validate it, I require the row's min and max value.

Validation Class:

public decimal Max { get; set; }

public decimal Min { get; set; }

public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
    var test = i < Min;
    var test2 = i > Max;

    if (test || test2)
        return new ValidationResult(false, String.Format("Fee out of range Min: ${0} Max: ${1}", Min, Max));
    else
        return new ValidationResult(true, null);
}

User Control:

<telerik:RadGridView SelectedItem ="{Binding SelectedScript}"
                     ItemsSource="{Binding ScheduleScripts}">
    <telerik:RadGridView.Columns>
        <telerik:GridViewDataColumn
            DataMemberBinding="{Binding Amount}" Header="Amount" 
            CellTemplate="{StaticResource AmountDataTemplate}" 
            CellEditTemplate="{StaticResource AmountDataTemplate}"/>   
        <telerik:GridViewComboBoxColumn
            Header="Fee Type" 
            Style="{StaticResource FeeTypeScriptStyle}" 
            CellTemplate="{StaticResource FeeTypeTemplate}"/>           
    </telerik:RadGridView.Columns>
</telerik:RadGridView>

FeeType Class:

public class FeeType
{
    public decimal Min { get; set; }
    public decimal Max { get; set; }
    public string Name { get; set; }
}

I've tried this solution here WPF ValidationRule with dependency property and it works great. But now I come across the issue that the proxy can't be instantiated through the viewmodel. It's based on the row's selected ComboBox Value's Min and Max property.

For example, that combo box sample values are below

Admin Min: $75 Max $500
Late  Min: $0  Max $50

Since a grid can have virtually as many rows as it wants, I can't see how creating proxies would work in my situation. If I can get some tips of guidance, would be greatly appreciated.

Community
  • 1
  • 1
Master
  • 2,038
  • 2
  • 27
  • 77
  • There is only one `ComboBox` in your code. – AnjumSKhan Apr 01 '17 at 06:16
  • @AnjumSKhan There's only suppose to be one ComboBox. The comboBox Values are of type FeeType class. So whatever is selected determines it's min and max. – Master Apr 03 '17 at 16:07
  • 4
    Are you sure you're that this isn't an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)? It's going to take a lot of effort to do this using a `ValidationRule`, whereas it would be fairly easy to accomplish if you moved the validation logic to the view-model. – Grx70 Apr 03 '17 at 18:57
  • @Grx70 I agree it's alot of work effort to complete this using ValidationRule but it improves the user experience to be notified of errors/Validation Fails. – Master Apr 05 '17 at 13:27
  • 1
    @Master What I meant was that rather than implementing and using custom `ValidationRule` you could put the validation logic in your view-model together with implementing `IDataErrorInfo`, and then use [`DataErrorValidationRule`](https://msdn.microsoft.com/en-us/library/system.windows.controls.dataerrorvalidationrule(v=vs.110).aspx) which would do the rest of the job. For _.Net 4.5_ or later you could also use `INotifyDataErrorInfo` + [`NotifyDataErrorValidationRule `](https://msdn.microsoft.com/en-us/library/system.windows.controls.notifydataerrorvalidationrule(v=vs.110).aspx) instead. – Grx70 Apr 06 '17 at 06:10

1 Answers1

4

Alert: this is not a definitive solution, but shows you a correct way to implement the validation logic putting it totally on ViewModels.

For semplicity purpose, I create the list of FeeTypes as static property of the FeeType class:

public class FeeType
{
    public decimal Min { get; set; }
    public decimal Max { get; set; }
    public string Name { get; set; }

    public static readonly FeeType[] List = new[]
    {
        new FeeType { Min = 0, Max = 10, Name = "Type1", },
        new FeeType { Min = 2, Max = 20, Name = "Type2", },
    };
}

This is the ViewModel for a single Grid row. I put only Amount and Fee properties.

public class RowViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public RowViewModel()
    {
        _errorFromProperty = new Dictionary<string, string>
        {
            { nameof(Fee), null },
            { nameof(Amount), null },
        };

        PropertyChanged += OnPropertyChanged;
    }

    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName)
        {
            case nameof(Fee):
                OnFeePropertyChanged();
                break;
            case nameof(Amount):
                OnAmountPropertyChanged();
                break;
            default:
                break;
        }
    }

    private void OnFeePropertyChanged()
    {
        if (Fee == null)
            _errorFromProperty[nameof(Fee)] = "You must select a Fee!";
        else
            _errorFromProperty[nameof(Fee)] = null;

        NotifyPropertyChanged(nameof(Amount));
    }

    private void OnAmountPropertyChanged()
    {
        if (Fee == null)
            return;

        if (Amount < Fee.Min || Amount > Fee.Max)
            _errorFromProperty[nameof(Amount)] = $"Amount must be between {Fee.Min} and {Fee.Max}!";
        else
            _errorFromProperty[nameof(Amount)] = null;
    }

    public decimal Amount
    {
        get { return _Amount; }
        set
        {
            if (_Amount != value)
            {
                _Amount = value;
                NotifyPropertyChanged();
            }
        }
    }
    private decimal _Amount;

    public FeeType Fee
    {
        get { return _Fee; }
        set
        {
            if (_Fee != value)
            {
                _Fee = value;
                NotifyPropertyChanged();
            }
        }
    }
    private FeeType _Fee;

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion

    #region INotifyDataErrorInfo
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool HasErrors
    {
        get
        {
            return _errorFromProperty.Values.Any(x => x != null);
        }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            return _errorFromProperty.Values;

        else if (_errorFromProperty.ContainsKey(propertyName))
        {
            if (_errorFromProperty[propertyName] == null)
                return null;
            else
                return new[] { _errorFromProperty[propertyName] };
        }

        else
            return null;
    }

    private Dictionary<string, string> _errorFromProperty;
    #endregion
}

Now, I tested it with a native DataGrid, but the result should be the same in Telerik:

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Rows}">
  <DataGrid.Columns>
    <DataGridTextColumn Binding="{Binding Amount}"/>
    <DataGridComboBoxColumn SelectedItemBinding="{Binding Fee, UpdateSourceTrigger=PropertyChanged}"
                            ItemsSource="{x:Static local:FeeType.List}"
                            DisplayMemberPath="Name"
                            Width="200"/>
  </DataGrid.Columns>
</DataGrid>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Rows = new List<RowViewModel>
        {
            new RowViewModel(),
            new RowViewModel(),
        };

        DataContext = this;
    }

    public List<RowViewModel> Rows { get; } 
}

If a FeeType instance can modify Min and Max at runtime, you need to implement INotifyPropertyChanged also on that class, handling the value changes appropriately.

If you're new to things "MVVM", "ViewModels", "Notification changes" etc, give a look to this article. If you usually work on middle-big project on WPF, it is worth learning how to decouple View and Logic through the MVVM pattern. This allows you to test the logic in a faster and more automatic way, and to keep things organized and focused.

Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
  • Although it may not be required in this particular example, it would be good practice to also raise `INotifyDataErrorInfo.ErrorsChanged` event. – Grx70 Apr 08 '17 at 14:01
  • @Grx70, well, actually WPF doesn't require you to raise the `ErrorsChanged` event in simple scenarios: it re-checks the errors when a `PropertyChanged` event is raised. You can see this if you copy my code in a VS solution and run it. Raising `ErrorsChanged` event is useful only in more complicated scenarios, and since my code was already long, I didn't want to make it even longer just to fulfill an issue that _in this case_ is just theoretical. – Massimiliano Kraus Apr 09 '17 at 11:51