0

I stuck with the validation process. On text change, the view model property (Name, the only one on what I am trying to achieve this) gets the value changed.

I am using CommunityToolkit.Mvvm v8.0.0.

My vie model looks like this:

public partial class PlayerModel : ObservableValidator
{
    private int id;
    private string name;
    private string localImageLink;
    private string webImageLink;
    private string club;
    private string birthday;
    private string birthPlace;
    private int weight;
    private double height;
    private string positionName;
    private int positionId;
    private string description;

    public ICommand ValidateCommand => new RelayCommand(() => ValidateAllProperties());

    public int Id
    {
        get => this.id;
        set => SetProperty(ref this.id, value, true);
    }

    [Required]
    [StringLength(255)]
    [MinLength(2)]
    public string Name
    {
        get => this.name;
        set
        {
            ClearErrors();
            ValidateAllProperties();

            SetProperty(ref this.name, value);

            _ = this[nameof(Name)];
        }
    }

    [StringLength(4096)]
    public string LocalImageLink
    {
        get => this.localImageLink;
        set => SetProperty(ref this.localImageLink, value, true);
    }

    [Required]
    [StringLength(4096)]
    public string WebImageLink
    {
        get => this.webImageLink;
        set => SetProperty(ref this.webImageLink, value, true);
    }

    [Required]
    [StringLength(255)]
    public string Club
    {
        get => this.club;
        set => SetProperty(ref this.club, value, true);
    }

    [Required]
    [StringLength(32)]
    public string Birthday
    {
        get => this.birthday;
        set => SetProperty(ref this.birthday, value, true);
    }

    [Required]
    [StringLength(255)]
    public string BirthPlace
    {
        get => this.birthPlace;
        set => SetProperty(ref this.birthPlace, value, true);
    }

    [Required]
    [Range(0, 100)]
    public int Weight
    {
        get => this.weight;
        set => SetProperty(ref this.weight, value, true);
    }

    [Required]
    [Range(0, 2.5)]
    public double Height
    {
        get => this.height;
        set => SetProperty(ref this.height, value, true);
    }

    [Required]
    public string Description
    {
        get => this.description;
        set => SetProperty(ref this.description, value, true);
    }

    public string PositionName
    {
        get => this.positionName;
        set => SetProperty(ref this.positionName, value, true);
    }

    [Required]
    [Range(1, 7)]
    public int PositionId
    {
        get => this.positionId;
        set => SetProperty(ref this.positionId, value, true);
    }

    public ValidationStatus this[string propertyName]
    {
        get
        {
            var errors = this.GetErrors()
                             .ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();

            var hasErrors = errors.TryGetValue(propertyName, out var error);
            return new ValidationStatus(hasErrors, error ?? string.Empty);
        }
    }
}

public class ValidationStatus: ObservableObject
{
    private bool hasError;
    private string error;

    public bool HasError
    {
        get => this.hasError;
        set => SetProperty(ref this.hasError, value);
    }

    public string Error
    {
        get => this.error;
        set => SetProperty(ref this.error, value);
    }

    public ValidationStatus()
    {
    }

    public ValidationStatus(bool hasError, string error)
    {
        this.hasError = hasError;
        this.error = error;
    }
}

I got stuck when I need to display a validation error (make the entry red, and show a label with a red text, for example). The entry field are always red, and the error message is never shown. When I debug public ValidationStatus this[string propertyName] get callaed, but I am not shore is it reflected back, or I am doing something wrong.

Part of my view:

VerticalStackLayout>
    <Label Text="Name" />
    <Entry x:Name="name" Text="{Binding Name, Mode=TwoWay}"
           ClearButtonVisibility="WhileEditing">
        <Entry.Behaviors>
            <toolkit:EventToCommandBehavior EventName="TextChanged"
                                            Command="{Binding ValidateCommand}" />
        </Entry.Behaviors>
        <Entry.Triggers>
            <DataTrigger TargetType="Entry"
                         Binding="{Binding [Name].HasError}"
                         Value="True">
                <Setter Property="BackgroundColor" Value="red" />
            </DataTrigger>
        </Entry.Triggers>
    </Entry>
    <Label Text="{Binding [Name].Error}"
           TextColor="red" />
</VerticalStackLayout>

If anyone done something like this, or could point me on the right directions. Another question is that I left from my view the following code, that is suggested in the official docks:

<Entry.Style>
  <OnPlatform x:TypeArguments="Style">
       <On Platform="iOS, Android" Value="{StaticResource EntryStyle}" />
       <On Platform="WinUI" Value="{StaticResource WinUIEntryStyle}" />
   </OnPlatform>
</Entry.Style>

Do I need them? What it's function?

thnx

Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • Could this help? [Validation in Enterprise Apps](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/validation) – Liqun Shen-MSFT Dec 30 '22 at 03:23
  • That is tha way that I am trying to avoid, where you have to write your own validators, ... – Wasyster Dec 30 '22 at 07:57

2 Answers2

2

It's about how to raise propertychanged for an indexer. I made a demo using two Properties in ViewModel. You could try the following code:

In .xaml, not necessary to use behaviors

<ScrollView>
    <VerticalStackLayout>
        <VerticalStackLayout>
            <Label Text="Name" />
            <Entry x:Name="name" Text="{Binding Name,Mode=TwoWay}"
               ClearButtonVisibility="WhileEditing">
            <Entry.Triggers>
                <DataTrigger TargetType="Entry"
                             Binding="{Binding [Name].HasError}"
                             Value="True">
                    <Setter Property="BackgroundColor" Value="Red" />
                </DataTrigger>
            </Entry.Triggers>
            </Entry>
            <Label  BackgroundColor="Yellow" Text="{Binding [Name].Error}"
               TextColor="Red" />
        </VerticalStackLayout>
        <VerticalStackLayout >
            <Label Text="Club" />
            <Entry x:Name="club" Text="{Binding Club,Mode=TwoWay}" ClearButtonVisibility="WhileEditing">
            <Entry.Triggers>
                <DataTrigger TargetType="Entry"
                             Binding="{Binding [Club].HasError}"
                             Value="True">
                    <Setter Property="BackgroundColor" Value="Red" />
                </DataTrigger>
            </Entry.Triggers>
            </Entry>
            <Label  BackgroundColor="Yellow" Text="{Binding [Club].Error}"
               TextColor="Red" />
        </VerticalStackLayout>
    </VerticalStackLayout>
</ScrollView>

In ViewModel, the most important is to Raise PropertyChanged for Indexer. That is OnPropertyChanged["Item[key]"].

public class MainPageViewModel : ObservableValidator
{

    private string name;
    private string club;

    [Required]
    [StringLength(255)]
    [MinLength(2)]
    public string Name
    {
        get => this.name;
        set
        {
            ClearErrors();              
            SetProperty(ref this.name, value);
            ValidateAllProperties();
            OnPropertyChanged("Item[Name]");
        }
    }

    [Required]
    [StringLength(255)]
    [MinLength(2)]
    public string Club
    {
        get => this.club;
        set
        {
            ClearErrors();
            SetProperty(ref this.club, value);
            ValidateAllProperties();
            OnPropertyChanged("Item[Club]");
        }
    }

    [IndexerName ("Item")]
    public ValidationStatus this[string propertyName]
    {
        get
        {
            var errors = this.GetErrors()
                             .ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();

            var hasErrors = errors.TryGetValue(propertyName, out var error);
            return new ValidationStatus(hasErrors, error ?? string.Empty);
        }
    }
}

Some useful info you could refer to:

PropertyChanged for indexer property

How do I use XAML to bind to a property inside a class instance in the view-model?

ObservableObject.OnPropertyChanged Method

Hope it works for you.

Liqun Shen-MSFT
  • 3,490
  • 2
  • 3
  • 11
0
<DataTrigger TargetType="Entry"
                     Binding="{Binding [Name].HasError}"
                     Value="False">
            <Setter Property="BackgroundColor" Value="red" />
</DataTrigger>

If your binding to HasError is False, the color turns red. Is this intended?

Also, you can have different styles for different OS. You are writing multiplatform code, you may want different UI for each platform. And you do not usually want this mobile UI on your WIN.

(But Maui has a long way to go, before everything renders the same. Right now the bugs are beyond count).

Edit: After inspecting your code more closely, I've noticed that nothing is notifying a change in your ValidationStatus.

You need such notification. Something should change a Property, so that the binding in the DataTrigger can fire.

Example. In the ObservableValidator we have the following fields:

    [Required(ErrorMessage = "Text is Required Field!")]
    [MinLength(5,ErrorMessage = "Text length is minimum 5!")]
    [MaxLength(10, ErrorMessage = "Text length is maximum 10!")]
    [ObservableProperty]
    string _text = "Hello";

    [ObservableProperty]
    bool _isTextValid;

    [ObservableProperty]
    string _error;

And when we try to validate them, we do this:

    [RelayCommand]
    void Validate()
    {
        ValidateAllProperties();
        
        if (HasErrors)
            Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
        else
            Error = String.Empty;

        IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
    }

(Sorry for not formatting it perfectly, just pointing concept here.)

In this situation, when we Validate the Text property, we also change the property, that indicates an error.

We can now use IsTextValid in our bindings, and it will be changed correctly.

H.A.H.
  • 2,104
  • 1
  • 8
  • 21