0
votes

I have a custom Xamarin Forms component with a Required property that I am setting to True in my viewmodel. In the constructor, I call a method CheckValidity() which checks whether the entry is required. For some reason, Required is showing up as false until I type in the entry (triggering the Text property to update) or click in or out of the entry (triggering the Unfocused event).

Any idea why the initial True value of Required is not taking effect until some activity occurs in my component? Thanks!

Use in View

<ui:ValidatingEntry Text="{Binding MyText}" Required="True" />

Component XAML

 <?xml version="1.0" encoding="UTF-8"?>
 <ContentView xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyPackage.ValidatingEntry">
     <ContentView.Content>
         <StackLayout x:Name="entryContainer">

             <Entry x:Name="entry" />

             <Label x:Name="message" />
         </StackLayout>
     </ContentView.Content>
 </ContentView>

Component C#

public partial class ValidatingEntry : ContentView
{
    private enum ValidationErrorType
    {
        NONE,
        REQUIRED,
        CUSTOM
    }

    private ValidationErrorType validationErrorType;

    public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(ValidatingEntry), default(string), BindingMode.TwoWay);

    public string Text
    {
        get
        {
            return (string)GetValue(TextProperty);
        }

        set
        {
            SetValue(TextProperty, value);

            Debug.WriteLine("set text to: " + value);

            CheckValidity();
            UpdateMessage();
        }
    }

    public static readonly BindableProperty RequiredProperty = BindableProperty.Create("Required", typeof(bool), typeof(ValidatingEntry), false);

    public bool Required
    {
        get
        {
            Debug.WriteLine("getting required property: " + (bool)GetValue(RequiredProperty));
            return (bool)GetValue(RequiredProperty);
        }

        set
        {
            SetValue(RequiredProperty, value);

            //THIS NEVER PRINTS
            Debug.WriteLine("set required property to: " + value);

            CheckValidity();
        }
    }

    public static readonly BindableProperty IsValidProperty = BindableProperty.Create("IsValid", typeof(bool), typeof(ValidatingEntry), true, BindingMode.OneWayToSource);

    public bool IsValid
    {
        get
        {
            return (bool)GetValue(IsValidProperty);
        }

        set
        {
            SetValue(IsValidProperty, value);
        }
    }

    private void CheckValidity()
    {
        Debug.WriteLine("checking validity");
        Debug.WriteLine("required? " + Required); //prints False until Entry is unfocused or user types in Entry
        Debug.WriteLine("string empty? " + string.IsNullOrEmpty(Text));

        if (Required && string.IsNullOrEmpty(Text))
        {
            Debug.WriteLine("required but not provided");
            IsValid = false;
            validationErrorType = ValidationErrorType.REQUIRED;
        }
        else
        {
            IsValid = true;
            validationErrorType = ValidationErrorType.NONE;
        }
    }

    private void UpdateMessage()
    {
        switch (validationErrorType)
        {
            case ValidationErrorType.NONE:
                message.Text = "";
                break;
            case ValidationErrorType.REQUIRED:
                message.Text = "This field is required.";
                break;
        }
    }

    public ValidatingEntry()
    {
        InitializeComponent();

        entry.SetBinding(Entry.TextProperty, new Binding("Text", source: this));

        CheckValidity(); //at this point, Required is always false

        entry.Unfocused += (sender, e) =>
        {
            CheckValidity();
            UpdateMessage();
        };
    }
}
2

2 Answers

1
votes

Properties of a type will not be updated with values from XAML until the constructor for the type returns, so you want to run CheckValidity after the constructor returns.

The easiest and quickest way to do this would be to launch a background thread to run CheckValidity as this will allow the constructor method to return and populate the properties with the values set in XAML. So try this:

public ValidatingEntry()
{
    InitializeComponent();

    entry.SetBinding(Entry.TextProperty, new Binding("Text", source: this));

    Task.Run(() => { 
        CheckValidity();
        UpdateMessage();
    });

    entry.Unfocused += (sender, e) =>
    {
        CheckValidity();
        UpdateMessage();
    };
}

It is worth noting that this is not exclusive to Forms. In a default (parameterless) constructor, only properties that have default values set will be already set when the constructor is running. So if you want the default to be true, set the default value in your BindableProperty.Create(...) method call for the Required property, e.g.:

public static readonly BindableProperty RequiredProperty = 
              BindableProperty.Create(
                  "Required", 
                  typeof(bool), 
                  typeof(ValidatingEntry), 
                  true);

As an example, one might think that when you do this:

 var x = new MyType { MyString = "New text" };

That MyString will be set in the constructor, but that is not true. The above is syntactic sugar that gets changed at compile time to the equivalent of:

 var x = new MyType();
 x.MyString = "New text";

So the constructor completes, and then the property is set.

However if you have a default value, e.g.:

public class MyType
{
    public string MyString { get; set; } = "Default text";
}

MyString will be set to "Default text" and available in the constructor.

An example Console app to demonstrate:

class MainClass
{
    public static void Main(string[] args)
    {

        var x = new MyType { MyString = "New text" };

        var y = Console.ReadKey();
    }
}

public class MyType
{
    public MyType()
    {
        Console.WriteLine($"Constructor: {MyString}");
        Task.Run(() => Console.WriteLine($"Task: {MyString}"));

    }

    public string MyString { get; set; } = "Default text";
}

output will be:

Constructor: Default text

Task: New text

1
votes

Try putting CheckValidity() in an eventhandler that is raised after the constructor is done, like BindingContextChanged:

public ValidatingEntry()
{
    InitializeComponent();
    ...
    BindingContextChanged += (sender, e) =>
    {
        CheckValidity();
    };
}