0

I need to display validation messages if a nested-component is not properly filled in. The component is consumed by other parent-components and they need to get feedback on whether there are validation issues.

I have tried the following code for the nested-component and used the CanSubmit method. While the method correctly tells if there are validation problems the validation messages are not showing.

All the code below can be tested on blzorrepl: https://blazorrepl.com/repl/GvOQlvvv1789ra1G37

@if(editContext != null) {
    <EditForm EditContext="@editContext">
        <input type="text" @bind="testModel.Name" />
        <DataAnnotationsValidator />
        <ValidationSummary />
    </EditForm>
}

@code {
    [Parameter]
    public TestModel testModel
    {
        get { return (TestModel)editContext?.Model; }
        set { editContext = new EditContext(value); }
    }

    EditContext editContext;

    public bool CanSubmit()
    {
        return editContext.Validate();
    }
}

This is my parent-component code, a bit reduced but reproducing the problem:

<ChildComponent @ref="myTestComponent" testModel="testModel" />
<input type="button" @onclick="buttonClick" value="validate programmatically" />
<div>@testMessage</div>

@code {
    TestModel testModel = new TestModel();
    ChildComponent myTestComponent;
    string testMessage;

    void buttonClick()
    {
        testMessage = "not passed validation";

        if (myTestComponent.CanSubmit())
        {
            testMessage = "passed validation!";
        }
    }
}

The testMessage is only used to show the validation status.

What can I do to make parent-component cause the nested-component to show validation messages? I can only place the submit in the parent-component.

As it was requested, here is a more complete example of what I am doing, a list of items which can be edited inline as well as a form to add more instances. https://blazorrepl.com/repl/mlYwlQPm34bekYE824

jakubiszon
  • 1,956
  • 1
  • 19
  • 33
  • you will need a Custom-Validator. EditForm only works with Built-In Forms Component like InputCheckBox, InputDate etc.. You can check the Micorsoft Documentation – JoeGER94 Feb 11 '21 at 11:05
  • @JoeGER94 if I put and click a `submit` input inside the `EditForm` I can get validation messages displayed. I don't see why I should use the built in components - how would they make validation messages start showing in this scenario? – jakubiszon Feb 11 '21 at 11:13
  • Is there a reason why you are using `````` instead of ``````? The component inherited from ```InputBase``` have many built-in features like validation. And if you change it to `````` it should work. Keep in mind that the field has to be modified before the ```ValidationSummary``` will show something. So adding a text, removing it, focus something else should trigger the validation. – Just the benno Feb 11 '21 at 12:17
  • @Justthebenno I want the error messages to show when I run the `CanSubmit` method. Updating the `input` to `InputText` shows validation message on the blur event but when I press the submit button that message is hidden once again. – jakubiszon Feb 11 '21 at 12:49
  • I wanted to know why you have decided to use this approach? Weren't you aware of ```InputText``` component or is there an underlying reason for your choice? That would lead to the next questions: Why have you decided to create the ```EditContext```, once the parameter of ```testModel``` is set? If you click on the button in the parent component, it will trigger a render cycle. During this cycle, the parameters will be set (again). So each time, the ```EditContext``` and its validation state will be reset. – Just the benno Feb 11 '21 at 13:53
  • @jakubiszon i really would like to help you. But i really dont understand your problem. You dont want to use "submit" ??? If you want to return a value from the ChildComponent you maybe could use a EventCallback. For more help i need a clear discription – JoeGER94 Feb 11 '21 at 15:50
  • @JoeGER94 I need the container component to initiate the validation. The child component will not contain the submit button because it can be used in multiple contexts. – jakubiszon Feb 11 '21 at 16:14
  • @jakubiszon i just used your code and it works as aspected. I receive the expected bool from CanSubmit and "not passed validation" or "passed validation" will be displayed – JoeGER94 Feb 11 '21 at 18:57
  • @JoeGER94 to put it really short - I want to be able to lunch validation and show validation messages when I need them. – jakubiszon Feb 12 '21 at 07:56
  • @jakubiszon maybe you can show what's NOT working as expceted. So i can help – JoeGER94 Feb 12 '21 at 08:08

3 Answers3

1

I'll try to explain why your approach is not working and then suggest ways to solve it. I hope I understand your intentions correctly.

First you need to change the <input type="text" ...> to <InputText @bind-Value="..." />

When your method buttonClick is finished in your parent component, Blazor will call StateHasChanged on your component. It's part of the built-in logic of an EventHandler. This will trigger the component life cycle of your child component. During that cycle, the setter of your child component property testModel will be called, again. Blazor doesn't make any test for equality. (The only mighty check engine is the DiffierentialRenderTree at the end of a render cycle). That means a new EditContext will be created. This context, though, doesn't know about the validation error. Hence the message disappears. To prove that point, set a counter variable inside the setter and display it on the page. You will see this result.

enter image description here.

To avoid this scenario, you create the EditContext once, when the parameters are set, for instance.

@code {

    [Parameter]
    public TestModel testModel { get; set; }

    EditContext editContext;

   protected override void OnParametersSet()
   {
       base.OnParametersSet();
       if(editContext == null)
       {
           editContext = new EditContext(testModel);
       }
   }

    public bool CanSubmit()
    {
        return editContext.Validate();
    }
}

If you need to update the model but preserve the validation state, write a comment, and we can go from there.

enter image description here

Just the benno
  • 888
  • 1
  • 7
  • Thanks, very useful. I updated the `setter` of `ChildComponent` to only set the `EditContext` when it is given a new instance of `TestModel`. It does exactly what I needed now, despite keeping ``. Here is the updated repl https://blazorrepl.com/repl/QFuGvmkX53o67uV758 Is that the best solution? Or would you suggest something else? – jakubiszon Feb 12 '21 at 09:58
  • I think you have a very special problem in mind and choosen a unique solution. Because I don't see or understand your context, I can't suggest "better" ways. @JoeGER94 solution with a cascading parameter could also be a good solution. Besides, I think writing about it increases the level of misunderstanding :). What I can offer you is a call/chat where you can show me the context and, hopefully, after that, I can answer your question. – Just the benno Feb 12 '21 at 11:10
  • @jakubiszon I personally think this solution is difficult to read and does a lot of things which wouldnt be necessary if you use my approach. Furthermore it could be problematic if you use it in different Parents as you descripted in your Question. Changes to a Cascading-Value will update in every consuming Child and reverse. – JoeGER94 Feb 12 '21 at 12:13
  • I have added another repl link at the bottom of my question. Basically I need to reference the same editor twice on the page and the set of buttons is different in each case. – jakubiszon Feb 12 '21 at 12:42
1

I have a different approach.. Your Parent have to look like this

<CascadingValue Value="testModel">
    <EditForm Model="testModel">
        <ChildComponent @ref="myTestComponent" />
        <ObjectGraphDataAnnotationsValidator />
        <ValidationMessage For="@(()=>testModel.Name)" />
        <br />
        <button type="submit">test</button>
    </EditForm>
</CascadingValue>

the only thing you need to do now in your Child is like this

@if (testModel != null)
{
  <InputText @bind-Value="testModel.Name" />
}

@code {

    [CascadingParameter]
    public TestModel testModel { get; set; }
}

I personally think it's less code...

JoeGER94
  • 129
  • 10
  • Very interesting. Here is my working REPL using your pproach https://blazorrepl.com/repl/wFkmbclL55yO5QFn38 It works by submitting a single EditForm. I think this is a little inflexible. The approach I used in my answer would allow using two separate child components (e.g. contact details + delivery address) to be accepted on one event. All I need to do is to implement and call validation methods ( in this case named `CanSubmit` ) on each child control to know if I can process both models. Otherwise I need to compose another model or use more submit buttons, am I correct? – jakubiszon Feb 12 '21 at 18:02
  • @jakubiszon well... my main approach would be more generalized than yours. First of all I wouldn't "hard code" the Childs. I would render them dynamically so it's more extendable and I would use some polymorphic code inside the Childs to prevent to write all the time the same code. We both use separate ways - both are possible... – JoeGER94 Feb 13 '21 at 13:02
  • @JoeGET94 that indeed sounds interesting. Do you know of any examples of such approach I could find online? – jakubiszon Feb 14 '21 at 14:01
  • 1
    @jakubiszon you can use the RenderTreeBuilder to dynamically Render Razor-Components. First of all check this https://blazor-university.com/templating-components-with-renderfragements/passing-data-to-a-renderfragement/ and then generate it like Steve Sanderson explained here: https://github.com/dotnet/aspnetcore/issues/16104#issuecomment-385713669 – JoeGER94 Feb 15 '21 at 09:05
0

What was the original problem was assigning the EditContext. It was necessary to access it but it can be done via editForm.EditContext as in the example below.

Working example https://blazorrepl.com/repl/Glkmbcbr323MP1VS55

Parent component:

<ChildComponent @ref="myTestComponent" testModel="testModel" />
<input type="button" @onclick="buttonClick" value="validate programmatically" />
<div>@testMessage</div>

@code {
    TestModel testModel = new TestModel();
    ChildComponent myTestComponent;
    string testMessage;

    void buttonClick()
    {
        testMessage = "not passed validation";

        if (myTestComponent.CanSubmit())
        {
            testMessage = "passed validation!";
        }
    }
}

Child component:

@if(testModel != null) {
    <EditForm @ref="editForm" Model="@testModel">
        <input type="text" @bind="testModel.Name" />
        <DataAnnotationsValidator />
        <ValidationSummary />
    </EditForm>
}

@code {
    [Parameter]
    public TestModel testModel
    {
        get; set;
    }

    EditForm editForm;

    public bool CanSubmit()
    {
        return editForm.EditContext.Validate();
    }
}
jakubiszon
  • 1,956
  • 1
  • 19
  • 33