Lyubomir Dimov
Lyubomir Dimov

Reputation: 141

Dynamic MVC Model Server side validation

I have the following view:

@model dynamic
<form class="form-horizontal" id="formDynamicItem" action="/DynamicItem/SaveItem" method="post">

    @Html.AntiForgeryToken()


    <div class="col-xs-12 buttonBar">
        <button type="submit" value="Save" name="submitButton" class="btn">Save</button>

        <button type="submit" value="Cancel" name="submitButton" class="btn">Cancel</button>
    </div>

    <div class="col-lg-6">
        <div class="ibox ">
            <div class="ibox-content">
                @{
                    foreach (var obj in Model)
                    {
                        var kvpObj = (KeyValuePair<string, object>)obj;
                        var entityProp = (EntityAttributeProperties)kvpObj.Value;
                        <div class="form-group">
                            @if (entityProp.IsHiddenField)
                            {
                                <input type="hidden" class="form-control" data-val="true" id="@kvpObj.Key" name="@kvpObj.Key" value="@entityProp.Value" />
                            }
                            else if (entityProp.IsFormField)
                            {
                                var isReadOnly = entityProp.IsReadonly ? "readonly" : "";
                                IHtmlString validationRules = Html.Raw(string.Empty);
                                if (entityProp.ValidationRules != null)
                                {
                                    validationRules = entityProp.ValidationRules;
                                }

                                @Html.Label(entityProp.Name, new { @class = labelClass })
                                <div class="@controlClass">
                                    @switch (@entityProp.Type)
                                    {
                                        //... many cases


                                        default:
                                            <input type="text" class="form-control" id="@kvpObj.Key" name="@kvpObj.Key" value="@entityProp.Value" @isReadOnly @validationRules />
                                            break;
                                    }
                                </div>
                            }
                        </div>
                    }
                }
            </div>
        </div>
    </div>
</form>




@section Scripts {
    <script>
        $("#formDynamicItem").validate();
    </script>
}

And in the controller I get my values using FormCollection:

public ActionResult SaveItem(FormCollection form)
        {
         ...
         newValue = typeConverter.ConvertFromString(form[entityAttribute.Name]);
         ...                                                   
         }    
   }

My question is the following:

How can I establish Server-side validation on such dynamic model? Can I use FormCollection somehow? Possibly build dynamic view model somehow? If anyone has experience in this please consider giving a suggestion(answer).

Update: Making Detail page with ViewModel insted of dynamic model

So, After much refactoring I appear to be again stuck with server-side validation:

So now I have this ViewModel:

public class DynamicItemViewModel
{
    public Guid Id { get; set; }
    public List<EntityAttributeProperties> Properties { get; set; }
}

this detail page:

@model ExactDistillation.Models.DynamicItem.DynamicItemViewModel
<div class="wrapper wrapper-content animated fadeInRight">
    <div class="row">
        @using (Html.BeginForm("SaveItem", "DynamicItem", FormMethod.Post, new { @class = "form-horizontal", @id = "formDynamicItem" }))
        {
            @Html.AntiForgeryToken()

            @Html.HiddenFor(x => x.Id)
            <div class="col-xs-12 buttonBar">
                <button type="submit" value="Save" name="submitButton" class="btn btn-primary pull-right">Save</button>

                <button type="submit" value="Cancel" name="submitButton" class="btn btn-default pull-right cancel">Cancel</button>
            </div>

            <div class="col-lg-6">
                <div class="ibox float-e-margins">
                    <div class="ibox-title text-webwonders">
                        <h5>Item</h5>
                    </div>
                    <div class="ibox-content">
                        @{
                            for (int i = 0; i < Model.Properties.Count; i++)
                            {
                                @Html.EditorFor(m => Model.Properties[i], "EntityAttributeProperties", "Properties[" + i + "]")
                            }
                        }
                    </div>
                </div>
            </div>
          }
    </div>
</div>

And this is how I define EntityAttributeProperties page:

@model EntityAttributeProperties

<div class="form-group">
    @if (Model.IsHiddenField)
    {
        @Html.HiddenFor(x => x.Value)
    }
    else if (Model.IsFormField)
    {
        @Html.Label(Model.Name, new { @class = "col-sm-5 col-md-4 col-lg-3" })

        <div class="col-sm-7 col-md-8 col-lg-9">
            @switch (Model.Type)
            {
                --- Many cases

                default:
                    @Html.DynamicTextBoxFor(m => m.Value, null, Model.IsReadonly, Model.ValidationRules)
                    break;

            }
        </div>
    }
</div>

EntityAttributesProperties looks the following way:

public class EntityAttributeProperties
{
    public string Name { get; set; }
    public string DisplayName { get; set; }
    public object Value { get; set; }
    public EntityAttributeDataTypeEnum Type { get; set; }
    public short Order { get; set; }
    public bool IsFormField { get; set; }
    public bool IsReadonly { get; set; }
    public bool IsHiddenField { get; set; }
    public Dictionary<string,object> ValidationRules { get; set; }
}

So, I am trying to make server-side validation for Model, but I am stuck, because I don't find any elegant solution to my problem, just solutions in which I have to do a lot of hardcoding (which I don't like).

Here is how I receive the form Submit:

 public ActionResult SaveItem(DynamicItemViewModel model)
        {
            List<EntityAttributeExtendedView> entityAttributes = GetItemEntityAttributes();
            DataObject dataObject = _dbContext.DataObjectCollection.FirstOrDefault(x => x.Id == model.Id);

            if (dataObject != null)
            {
                JObject json = JObject.Parse(dataObject.Value);
                dynamic dynJson = JsonConvert.DeserializeObject(dataObject.Value);

                // Possibly loop through all entity attributes and separately make a custom validation ?
                // Or somehow create custom Model State for validation ?

            }
            return View("Detail", model);


        }

I will appreciate any suggestion on how to approach the problem with server side validation.

Thanks.

Upvotes: 0

Views: 801

Answers (2)

thmshd
thmshd

Reputation: 5847

I'm adding another Answer because the Question changed heavily.

A good approach is to use the IValidatableObject Interface. So you add this Interface to your EntityAttributeProperties class and have to override the Method Validate. for simple validation like required fields, you use so called validation Attributes.

Your EntityAttributeProperties class would be decorated like this:

public class EntityAttributeProperties : IValidatableObject
{
    public string Name { get; set; }

    [Required]
    public object Value { get; set; }

    ...

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        if (... /* some condition, e.g. specific EntityAttributeDataTypeEnum */)
        {
            // Do some validation

            // some other random test
            if (.../* something not right... */)
            {
                results.Add(new ValidationResult("your input was not valid!"));
            }
        }

        return results;
    }
}

You might need to also make your DynamicItemViewModel and IValidatableObject and loop through the Items, but sometimes MVC is smart enough to validate sub-items automatically, so you might need this:

 public class DynamicItemViewModel : IValidatableObject
{
    ...

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        return Items.SelectMany(x => x.Validate(validationContext));
    }
}

OK now in your Controller, you basically need to check your ModelState. The automatically generated ModelState Property contains all errors.

Upvotes: 1

thmshd
thmshd

Reputation: 5847

FormCollection is a very raw form of data and cannot be validated easily. I'd suggest you should refactor your view to be able to use ViewModels, else you will have a hard time working with the Data.

I cannot show you the full way but give you some hints:

  1. Create a single View Model class for the View which contains a list of Items (of type EntityAttributeProperties). Let's call it MainViewModel.
  2. Pass this View Model to the view rather then the Dictionary
  3. In your view, use @Html.EditoFor(x => x.Items) to generate the correct HTML. ASP.NET MVC will use the Editor Templates for EntityAttributeProperties type
  4. This is a good moment to create a new view EntityAttributeProperties.cshtml in your View folders EditorTemplates sub-folder
  5. within this item view, you can do all your entityProp.Type switches, but be careful with ID generation, always use @Html.IdFor(...) etc. instead of generating own IDs to keep type safe with your View Model
  6. After some tweaks, your Post Action should be able to receive your view model of Type MainViewModel. If everything went good, the Item's will be filled, even if you used different Controls (hidden fields, text fields, drop-downs...) to populate the Values

From my perspective, only this MVC-safe approach will lead to success in this case

Upvotes: 2

Related Questions