1

I am trying to create properties in my model programmatically when the application is run. I've tried to follow the answer by Darin Dimitrov on this post How to create controls dynamically in MVC 3 based on an XML file

I am trying to convert the code to VB.NET. So far I have...

Model:

Public Class MyViewModel
    Public Property Controls As ControlViewModel()
End Class

Public MustInherit Class ControlViewModel
    Public MustOverride ReadOnly Property Type As String
    Public Property Visible As Boolean
    Public Property Label As String
    Public Property Name As String
End Class

Public Class TextBoxViewModel
    Inherits ControlViewModel
    Public Overrides ReadOnly Property Type As String
        Get
            Return "textbox"
        End Get
    End Property
    Public Property Value As String
End Class

Public Class CheckBoxViewModel
    Inherits ControlViewModel
    Public Overrides ReadOnly Property Type As String
        Get
            Return "checkbox"
        End Get
    End Property
    Public Property Value As Boolean
End Class

Controller:

Function Test() As ActionResult

    Dim model = New MyViewModel() With { _
    .Controls = New ControlViewModel() {New TextBoxViewModel() With { _
        .Visible = True, _
        .Label = "text label", _
        .Name = "TextBox1", _
        .Value = "Text appears here" _
    }, New CheckBoxViewModel() With { _
        .Visible = True, _
        .Label = "check label", _
        .Name = "CheckBox1", _
        .Value = True _
    }
    }}

    Return View("Test", model)

End Function

<httpPost()>
Function Test(model As MyViewModel) As ActionResult

    Return View("Test", model)

End Function

View:

@ModelType MyApp.DomainModel.MyTest.MyViewModel

@Code
Using Html.BeginForm()

Dim i As Integer
For i = 0 To Model.Controls.Length - 1
End Code
    <div> 
        @Html.EditorFor(Function(model) model.Controls(i)) 
    </div> 
@Code
Next
End Code

<input type="submit" value="OK" /> 

@Code
End Using
End Code

TextBox Editor Template:

@modeltype MyApp.DomainModel.MyTest.TextBoxViewModel

@Html.LabelFor(Function(model) model.Value, Model.Label) 
@Html.TextBoxFor(Function(model) model.Value) 

Checkbox Editor Template:

@modeltype MyApp.DomainModel.MyTest.CheckBoxViewModel

@Html.LabelFor(Function(model) model.Value, Model.Label) 
@Html.CheckBoxFor(Function(model) model.Value) 

Custom Model Binder:

Public Class ControlModelBinder
    Inherits DefaultModelBinder

    Public Overrides Function BindModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext) As Object
        Dim type = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type")
        Dim name = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Name")
        Dim value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Value")

        If type IsNot Nothing AndAlso value IsNot Nothing Then
            Select Case type.AttemptedValue
                Case "textbox"
                    Return New TextBoxViewModel() With { _
                          .Name = name.AttemptedValue, _
                          .Value = value.AttemptedValue _
                        }
                Case "checkbox"
                    Return New CheckBoxViewModel() With { _
                          .Name = name.AttemptedValue, _
                          .Value = Boolean.Parse(value.AttemptedValue.Split(","c).First()) _
                        }
            End Select
        End If

        Throw New NotImplementedException()

    End Function
End Class

Global.asax:

ModelBinders.Binders.Add(GetType(MyTest.ControlViewModel), New MyTest.ControlModelBinder())

When I run the application, in the custom model binder the type and name variable don't seem to be set correctly.

What could I be doing wrong?

Community
  • 1
  • 1
cw_dev
  • 465
  • 1
  • 12
  • 27

1 Answers1

2

You are missing 2 hidden fields in your main view:

@ModelType MyApp.DomainModel.MyTest.MyViewModel

@Using Html.BeginForm()
    For i = 0 To Model.Controls.Length - 1
        @<div> 
            @Html.HiddenFor(Function(model) model.Controls(i).Type) 
            @Html.HiddenFor(Function(model) model.Controls(i).Name) 
            @Html.EditorFor(Function(model) model.Controls(i)) 
        </div> 
    Next
    @<input type="submit" value="OK" /> 
End Using

In your code you only have a single EditorFor call for the Controls property but you never specify the type of the control through a hidden field which is used by the custom model binder.


UPDATE:

I have also fixed the original model binder which contained a bug as it is better to override the CreateModel method instead of the BindModel:

Public Class ControlModelBinder
Inherits DefaultModelBinder
Protected Overrides Function CreateModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext, modelType As Type) As Object
    Dim type = bindingContext.ValueProvider.GetValue(Convert.ToString(bindingContext.ModelName) & ".Type")
    If type Is Nothing Then
        Throw New Exception("The type must be specified")
    End If

    Dim model As Object = Nothing
    Select Case type.AttemptedValue
        Case "textbox"
            If True Then
                model = New TextBoxViewModel()
                Exit Select
            End If
        Case "checkbox"
            If True Then
                model = New CheckBoxViewModel()
                Exit Select
            End If
        Case "ddl"
            If True Then
                model = New DropDownListViewModel()
                Exit Select
            End If
        Case Else
            If True Then
                Throw New NotImplementedException()
            End If
    End Select

    bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(Function() model, model.[GetType]())
    Return model
End Function
End Class
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I am now having trouble creating my model when it comes from a database. The sample code creating a few controls works fine, but I am not sure how I can do it from a database. I tried to declare a variable as a New ControlViewModel but this doesn't work because of the Abstract Class. Any idea how I can map a datatable to the model? – cw_dev Mar 07 '12 at 15:18
  • @cw_dev, I cannot answer this question as you didn't provide any details about your exact database schema and what database access technology you are using. Probably worth starting a new question as this is no longer related to ASP.NET MVC but more to database access in .NET. – Darin Dimitrov Mar 07 '12 at 15:29
  • Okay thanks. I did manage to get it working by doing something like this. Note sure if it's best but this does work. Dim myList As New List(Of DataTitleControlsModel)() myList.Add(t) myList.Add(t2) myList.Add(t3) datatitlesmodel = New DataTitlesModel() With {.Controls = myList.ToArray} – cw_dev Mar 07 '12 at 15:58
  • I see the comments strip out my line breaks but hopefully you can see what I am doing. It wasn't the DB bit I was having problems with, I was just trying to figure out how to add controls using the .Add line. – cw_dev Mar 07 '12 at 16:02
  • @cw_dev, you could declare a `List(Of ControlViewModel)` variable and then add elements to it: `list.Add(t1) list.Add(t2) ...` where `t1` and `t2` must be instances of `ControlViewModel` or more precisely one of the derived classes. Once the list is populated, you could simply assign it to the `Controls` property of your view model. – Darin Dimitrov Mar 07 '12 at 16:13
  • Just one more question regarding this post... I assigned the Required data annotation attribute to the TextBoxViewModel. I am using unobtrusive validation. Client side validation works perfectly, but if I disable JavaScript in my browser the form allows a blank entry. I presume this is because of the way the control uses BindModel. Is there a way around this? The easy solution seems to be to manually check values in the BindModel function but don't know if there is another way. – cw_dev Mar 08 '12 at 16:06
  • @cw_dev, on which property of the view model did you put this attribute? Of what type is the property? – Darin Dimitrov Mar 08 '12 at 16:35
  • I put it on the Value property within the TextBoxViewModel (Public Property Value As String). I assume this was the correct place because the client side is working. – cw_dev Mar 08 '12 at 16:43
  • Like this: ` Public Property Value As String`? – Darin Dimitrov Mar 08 '12 at 16:53
  • Yeah, essentially. on the first line then Public Property Value As String on the second line. I just created a completely blank project with the sample as in the original post with your initial fix in and then added the Required attribute and it works client side but not server side. – cw_dev Mar 08 '12 at 17:05
  • @cw_dev, indeed, there was a bug in my initial model binder. I have fixed it now. You should override the `CreateModel` method and not the `BindModel` method in the model binder. See my updated answer. – Darin Dimitrov Mar 08 '12 at 17:26