Home > Net >  Blazor: How to facilitate that a viewmodel is initialized with the correct state?
Blazor: How to facilitate that a viewmodel is initialized with the correct state?

Time:11-21

If You are familiar with blazor You will know that EditForm components* need a model provided to them. In such case the provided ViewModel is used both for creation of a new entity and an existing one that you pull from the database. In one case therefor the ViewModel has the Id == null and another where it has the Id of the existing entity. What is the best way to let the ViewModel be created?

One could be this with two constructors:

    public class PersonViewModel
    {
        public PersonViewModel(String name, Int32 age)
        {
            if (String.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"{nameof(name)} is invalid");
            if (age < 0) throw new InvalidOperationException($"{nameof(age)} is invalid");

            Name = name;
            Age = age;
        }

        public PersonViewModel(Guid id, String name, Int32 age)
        {
            if (id == Guid.Empty) throw new InvalidOperationException($"{nameof(id)} is invalid");
            if (String.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"{nameof(name)} is invalid");
            if (age < 0) throw new InvalidOperationException($"{nameof(age)} is invalid");

            Id = id;
            Name = name;
            Age = age;
        }

        public Guid? Id { get; }
        public String Name { get; }
        public Int32 Age { get; }
    }

Here there is the repetion of the validation as an issue.

Another one would be with two different static method and making the constructor private, this ofc introduce some disadvantages like you would not be able to inherit the class and use base constructor:

    public class PersonViewModel
    {
        private PersonViewModel() { }

        public Guid? Id { get; private set; }
        public String Name { get; init; }
        public Int32 Age { get; init; }

        public static PersonViewModel MakeWithoutId(String name, Int32 age)
        {
            if (String.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"{nameof(name)} is invalid");
            if (age < 0) throw new InvalidOperationException($"{nameof(age)} is invalid");

            return new()
            {
                Name = name,
                Age = age,
            };
        }

        public static PersonViewModel MakeWithId(Guid id, String name, Int32 age)
        {
            if (id == Guid.Empty) throw new InvalidOperationException($"{nameof(id)} is invalid");

            var vm = PersonViewModel.MakeWithoutId(name, age);
            vm.Id = id;
            return vm;
        }
    }

I guess one could say that having the id as private set instead of init is bad and I could have just initialized it from scratch again but in this way I reuse the code and validation from the previous method. Or should I have two separated ViewModel classes where one inherits or contains the other like NewPersonViewModel and ExistingPersonViewModel?

  • Now that i think about it the property used in the EditForm inputs should have the public set, sorry for the mistake I was trying this code in a random file

CodePudding user response:

I would use something like this

public class PersonViewModel
{
    public Guid? Id { get; private set; }
    public String Name { get; private set; }
    public Int32 Age { get; private set; }
    
    private void Validate(Guid id, String name, Int32 age)
    {
        if (id == Guid.Empty) throw new InvalidOperationException($"{nameof(id)} is invalid");
        
        Id = id;

        Validate(name, age);
    }

    private void Validate(String name, Int32 age)
    {
        if (String.IsNullOrWhiteSpace(name)) throw new InvalidOperationException($"{nameof(name)} is invalid");
        if (age < 0) throw new InvalidOperationException($"{nameof(age)} is invalid");

        Name = name;
        Age = age;
    }
    
    public PersonViewModel(String name, Int32 age)
    {
        Validate( name, age);
    }

    public PersonViewModel(Guid id, String name, Int32 age)
    {
        Validate(id, name, age);
    }

    public static PersonViewModel GetInstance(String name, Int32 age)
    {
         return new PersonViewModel(name,age);
        
    }

    public static PersonViewModel GetInstance(Guid id, String name, Int32 age)
    {
        return new PersonViewModel(id,name,age);
    }

}

test

var pvm=PersonViewModel.GetInstance("New Name", 20); //ok
var pvm1=PersonViewModel.GetInstance(Guid.NewGuid(), "New Name", -1); //invalid age exception

CodePudding user response:

Using private set; in a ViewModel isn't very useful.

A ViewModel needs validation after it was edited, not so much on creation. And you want Validation to produce clear message for user feedback, not exceptions.

I would prefer:

public class PersonViewModel 
{
   public PersonViewModel() { }  // can be omitted

   public Guid? Id { get; set; }
   // ...

   public string[] Validate() { ... }
}

The logic you are trying to apply is fine for Entities, you should prevent creating invalid ones.

CodePudding user response:

That is not the way to work with the EditForm...

You should create a simple POCO class, without validation of any kind. Use DataAnnotation validation or Fluent validation in your EditForm.

Here's a code snippet demonstrating how to initialize the model object with data from the database, and how to enable adding a new instance of the model class for a new entry.

Comment.cs

public class Comment
{
    public string ID { get; set; } 
    public string Name { get; set; } 
    [MaxLength(100)]
    public string Text { get; set; } 

} 

Index.razor

<p>Leave a message if you liked my answer</p>

<EditForm EditContext="@EditContext" OnValidSubmit="@HandleValidSubmit" 
                    OnInvalidSubmit="@HandleInvalidSubmit">
    <div class="alert @StatusClass">@StatusMessage</div>

    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">
        <label for="name">Name: </label>
        <InputText Id="name" Class="form-control" @bind- 
                        Value="@Model.Name"></InputText>
        <ValidationMessage For="@(() => Model.Name)" />
    </div>
    <div class="form-group">
        <label for="body">Text: </label>
        <InputTextArea Id="body" Class="form-control" @bind- 
                             Value="@Model.Text"></InputTextArea>
        <ValidationMessage For="@(() => Model.Text)" />
    </div>
    <button type="submit">Ok</button>

</EditForm>

code
{
    private string StatusMessage;
    private string StatusClass;

    private EditContext EditContext;
    private Comment Model;

    protected override void OnInitialized()
    {
        // Here you put code from the database that instantiate the
        // Model object, and populate it with data from external store

        // You then instantiate the EditContext object pssing it the
        // created model object. Now the data from the database is 
       // displayed in the form.
        EditContext = new EditContext(Model);
        
        base.OnInitialized();
    }

    protected void HandleValidSubmit()
    {
        // HandleValidSubmit is called when the user click the "submit"
    // button. You can put here code that update the data in the 
    // data store, and then instantiate the EditContext object with
    // a new model instance, which may be a new record from the 
    // database or a new empty instance of the model object; in our 
    // code it is the Comment class.
    // Here's a code snippet demonstrating how to reset your form,
    // to enable a new entry of comment:
    //               Model = new Comment();
    //               EditContext = new EditContext(Model);
    
    // That is the way to ensure that 
    // your EditContext (Not EditForm) contains a new model object
    // with default values. This is the heart of my answer to the 
    // issue you were facing. If it not clear, please ask...

        StatusClass = "alert-info";
        StatusMessage = DateTime.Now   " Handle valid submit";
    }

    protected void HandleInvalidSubmit()
    {
        StatusClass = "alert-danger";
        StatusMessage = DateTime.Now   " Handle invalid submit";
    }
}
  • Related