0

I'm really new to ASP.NET MVC and basically everything web related. Sorry if this is nooby but I'm trying to do this:

I have a ViewModel with a complex property (navigation here):

public class Request
{
    public virtual BaseRequestData RequestData { get; set; }
}

BaseRequestData is abstract but has a couple classes inherited from it, which have other attributes like this one:

public class AcceleratorRequestData : BaseRequestData
{
    [Display(Name="Downside amount")]
    [Range(-100,0,ErrorMessage = "Downside participation must be between 0 and -100")]
    [Required]
    public decimal PutNotional { get; set; }

    [Display(Name="Upside strike")]
    [Range(1, 2, ErrorMessage = "Upside strike must be between 100% and 200%")]
    public decimal CallPercentStrike { get; set; }

}

On my main 'Create' view I bind to my Request model but I want to make a partial view for my BaseRequestData depending on the type it is (for instance, AcceleratorRequestData)via the user selecting it from a dropdown. What I've tried is using some jQuery to call a controller and render a partial view depending on the dropdown. Here's one of my partial views which is a bunch of form-groups:

 @model Synapse.Models.AcceleratorRequestData

@Html.ValidationSummary(true,"",new {@class = "text-danger"})
<div class="form-group">
    @Html.LabelFor(m => Model.PutNotional, new {@class = "control-label col-md-2"})
    <div class="col-md-10">
        @Html.EditorFor(model => model.PutNotional, new {htmlAttributes = new {@class = "form-control"}})
        @Html.ValidationMessageFor(model => model.PutNotional, "", new {@class = "text-danger"})
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => Model.CallPercentStrike, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.CallPercentStrike, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.CallPercentStrike, "", new { @class = "text-danger" })
    </div>
</div>

Which replaces my placeholder <div> in my main view:

@using (Html.BeginHorizontalForm())
{
    @Html.AntiForgeryToken()

    <hr/>
    @Html.ValidationSummary(true, "", new {@class = "text-danger"})
    <div id="accelRequestPh" style="display: none;"></div>

    <input type="submit" value="Create" class="btn btn-default" />

}

But here my validation properties and bindings fail (obviously, because my BaseRequestData object isn't bound to the instance of my Request object). How can I do this? Should I use an editor template? If I do, still, how will my main view model know about them?

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
coolboyjules
  • 2,300
  • 4
  • 22
  • 42
  • What's the purpose of having the `Request` class containing the `BaseRequest` as a virtual property? I ask because you don't use the `Request` object anywhere (you can't validate what's not there). – Stephen P. Sep 28 '16 at 17:35
  • I need it for navigation. My parent object is type Request, that I care about generating, and each Request has a BaseRequestData object that will be concrete. – coolboyjules Sep 28 '16 at 17:39
  • Would it be better to use an interface for your abstract definition and have whatever is using `Request` consume the interface instead? I ask because you say your validation breaks because BaseRequest isn't bound to Request, but the way you've designed this code it's hard to tell what's going on without seeing the whole source and how everything is consuming your dependency chain (at least for me it is). – Stephen P. Sep 28 '16 at 17:46
  • Sorry. So basically I have an index page and I want a 'new' button which will create a Request object in a dynamically created form with fields depending on the BaseRequestData type selected in a dropdown. I think I'm just going to go with something simple for now and just have each dropdown link return a view which has a the abstract type concrete. – coolboyjules Sep 28 '16 at 17:52

2 Answers2

0

Add AcceleratorRequestData to your Request class:

    public class Request
{
    public virtual BaseRequestData RequestData { get; set; }
    public  AcceleratorRequestData acceleratorRequestData { get; set; }
}

Then change model of your partial view:

@model Synapse.Models.Request.AcceleratorRequestData
Hadee
  • 1,392
  • 1
  • 14
  • 25
  • But that would defeat the purpose of my abstract class :( – coolboyjules Sep 28 '16 at 19:47
  • Why you think "it defeat the purpose of my abstract class"? It is just using a class as a property for other class. It isn't about your class inheritance. – Hadee Sep 28 '16 at 19:49
  • Yes but the point is my RequestData will morph into its children. Otherwise I could have just removed the abstract class and added all the children manually like this... – coolboyjules Sep 28 '16 at 19:55
  • I know what you mean but model binding in MVC doesn't work that way. you need have different class types as properties inside your `Request` class based on user interaction. Then you could have fully support for data annotation validation and easier model binding. Other wise you need think about how you want bind user request to different types. – Hadee Sep 28 '16 at 20:01
  • I managed to get something working here: http://stackoverflow.com/questions/6484972/viewmodel-with-listbaseclass-and-editor-templates – coolboyjules Sep 28 '16 at 20:05
0

So I gave up the dropdown rendering different html elements and instead I just moved the dropdown the previous page so that it passes the concrete subclass to the view. Like this:

My dropdown calls this controller:

    /// <summary>
    /// Get the correct view depending on the dropdown
    /// </summary>
    /// <param name="productType"></param>
    /// <returns></returns>
    [HttpGet]
    public ActionResult Create(string productType)
    {
        Request request = new Request();
        switch (productType)
        {
            case "Accelerator":
                request.RequestData = new AcceleratorRequestData();
                request.ColloquialType = ColloquialType.Accelerator;
                return View(request);
            case "BarrierAccelerator":
                request.RequestData = new BarrierAcceleratorRequestData();
                request.ColloquialType = ColloquialType.BarrierAccelerator;
                return View(request);
            default:
                return RedirectToAction("Index", "Requests");
        }
    }

Which returns a parent view:

@using (Html.BeginHorizontalForm())
{
    @Html.AntiForgeryToken()

    <hr/>
    @Html.ValidationSummary(true, "", new {@class = "text-danger"})
    @Html.EditorFor(x => x.RequestData)

    <input type="submit" value="Create" class="btn btn-default"/>
}

I use editor templates here for each of my children of BaseRequestData. Here's one:

@model Synapse.Models.AcceleratorRequestData

@Html.Hidden("ModelType", Model.GetType())           
@Html.ValidationSummary(true,"",new {@class = "text-danger"})
<div class="form-group">
    @Html.LabelFor(m => Model.PutNotional, new {@class = "control-label col-md-2"})
    <div class="col-md-10">
        @Html.EditorFor(model => model.PutNotional, new {htmlAttributes = new {@class = "form-control"}})
        @Html.ValidationMessageFor(model => model.PutNotional, "", new {@class = "text-danger"})
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => Model.CallPercentStrike, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.CallPercentStrike, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.CallPercentStrike, "", new { @class = "text-danger" })
    </div>
</div>

The hidden html helper is from this link: ViewModel with List<BaseClass> and editor templates

And I also had to edit my global.cs

 public class MvcApplication : HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            log4net.Config.XmlConfigurator.Configure(new FileInfo(Server.MapPath("~/Web.config")));
            ModelBinders.Binders.Add(typeof(BaseRequestData), new BaseRequestDataModelBinder());
        }
    }

    public class BaseRequestDataModelBinder : DefaultModelBinder
    {
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            ValueProviderResult typeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".ModelType");
            Type type = Type.GetType(
                (string)typeValue.ConvertTo(typeof(string)),
                true
            );
            object model = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
            return model;
        }
    }

Hopefully this helps someone!

Community
  • 1
  • 1
coolboyjules
  • 2,300
  • 4
  • 22
  • 42