6

Using @Html.EditorFor(model =>model.IsClient), where IsClient is a boolean, renders a drop down list with Not Set, Yes and No as the options.

All well and good.

Now I want to use knockoutjs with the resulting dropdownlist, that I like, so how do I add the data-bind attribute using @Html.EditorFor, that I need for knockoutjs to work with this drop down?

I have tried:

@Html.EditorFor(model => model.IsClient, new Dictionary<string, object> { { "data-bind", "value: Account.IsClient" } })

However, this uses the object additionalViewData parameter, and it doesn't render the data-bind attribute. Which is probably quite natural, as this parameter is probably nothing to do with Html Attributes for the rendered tag.

However, can't find any reasonable documentation, and none of the other overloads look likely candidates for what I want.

TIA any suggestions.

awrigley
  • 13,481
  • 10
  • 83
  • 129
  • There is a much simpler solution: http://stackoverflow.com/questions/6525838/custom-attribute-with-dash-in-name-using-editorfor-textboxfor-textbox-helpers – Shane Sep 29 '12 at 14:33

2 Answers2

10

Brad Wilson blogged about display and editor templates in ASP.NET MVC 2. So you could modify the default template for boolean and add the attributes you need (~/Views/Shared/EditorTemplates/MyTemplate.cshtml):

@{
    bool? value = null;
    if (ViewData.Model != null) 
    {
        value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
    }

    var triStateValues = new List<SelectListItem> 
    {
        new SelectListItem 
        { 
            Text = "Not Set",
            Value = String.Empty,
            Selected = !value.HasValue 
        },
        new SelectListItem 
        { 
            Text = "True",
            Value = "true",
            Selected = value.HasValue && value.Value 
        },
        new SelectListItem 
        { 
            Text = "False",
            Value = "false",
            Selected = value.HasValue && !value.Value 
        },
    };
}

@if (ViewData.ModelMetadata.IsNullableValueType) 
{
    <!-- TODO: here you can use any attributes you like -->
    @Html.DropDownList(
        "", 
        triStateValues, 
        new { 
            @class = "list-box tri-state", 
            data_bind="value: " + ViewData.TemplateInfo.GetFullHtmlFieldName("") // you could also use ViewData.ModelMetadata.PropertyName if you want to get only the property name and not the entire navigation hierarchy name
        }
    )
} 
else 
{
    @Html.CheckBox("", value ?? false, new { @class = "check-box" })
}

and finally:

@Html.EditorFor(model => model.IsClient, "MyTemplate")

or decorate the IsClient property on your view model with the UIHint attribute:

[UIHint("MyTemplate")]
public bool? IsClient { get; set; }

and then:

 @Html.EditorFor(x => x.IsClient)

will automatically pick the custom editor template.

Scott Lawrence
  • 6,993
  • 12
  • 46
  • 64
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Darin: thanks for the detail and the link to the Blog. The problem is that the data-bind attribute requires me to specify the name of the observable I am binding to. It seems like this cuts out that, as no way of passing in that name. Is there a way of using additionalViewData for this purpose? Otherwise, I will just have to use Html.DropDownList, or just write the select tag "manually". – awrigley Sep 05 '11 at 08:02
  • Darin: if you add a note as to why the object additionalViewData parameter cannot / should not be used for this purpose, I will happily mark your answer as accepted. – awrigley Sep 05 '11 at 08:05
  • @awrigley, if you need to pass this information from the main view to the editor template you could indeed use the `additionalViewData` parameter. In your case this would be the 3rd parameter if you decide to specify the editor name. Why do you think that this parameter should not be used? – Darin Dimitrov Sep 05 '11 at 08:10
  • I tried it and it seemed to have no effect, so presumed it had not liked what I passed in: new Dictionary{{"data-bind", "value: IsClient"}} (see my post) – awrigley Sep 05 '11 at 08:13
  • @awrigley, so all you need is to pass the property name IsClient? – Darin Dimitrov Sep 05 '11 at 08:14
  • a wee bit more. I need it to add an attribute to the rendered select tag, as follows: data-bind="value: IsClient" – awrigley Sep 05 '11 at 08:16
  • @awrigley, so do this in your template: `@Html.DropDownList("", triStateValues, new { @class = "list-box tri-state", data_bind: "value: " + ViewData.TemplateInfo.GetFullHtmlFieldName("") })`. `ViewData.TemplateInfo.GetFullHtmlFieldName` will give you want you need and you will no longer need to use the `additionalViewData` parameter. I have updated my answer to illustrate this. – Darin Dimitrov Sep 05 '11 at 08:19
  • Thanks, will test that and see. It does tie me to a convention, ie, that the knockoutjs observable name matches the model property name, but hey, this is MVC. – awrigley Sep 05 '11 at 08:28
  • Thanks, that works, and I have learnt a lot. The problem is when the view is a little more complex and I need the observables in an object. In that case, instead of just the name of the model property, I need to add the object name. EG, Account.IsClient. I will probably use the Html.DropDownList helper, but thanks for a very instructive answer. – awrigley Sep 05 '11 at 08:33
  • @awrigley, the `ViewData.TemplateInfo.GetFullHtmlFieldName` does exactly that. If you have comlpex property it will take it into account and return `Account.IsClient`. – Darin Dimitrov Sep 05 '11 at 08:34
  • You have a typo in your code (where you check that the ModelMetadata is nullable value type): 'data-bind:' should be 'data-bind=' - I will correct it. – awrigley Sep 05 '11 at 09:31
5

Addendum for knockoutjs users:

@Darin Dimitrov's answer is great, but slightly too rigid to use with knockoutjs, where complex views may lead to viewModels that don't entirely map to the @Model parameter.

So I have made use of the object additionalViewData parameter. To access the additionalViewData parameter from your Custom EditorTemplate, see the following SO question:

Access additionalViewData from Custom EditorTemplate code

Digression: The additionalViewData param is confusing in that it does nothing with the default editor. It only comes into its own with a custom editor template.

Anyway, my amendments to Darin's code are as follows:

@if (ViewData.ModelMetadata.IsNullableValueType) 
{
    var x = ViewData["koObservablePrefix"];
    if ((x != "") && (x != null)) { x = x + "."; }
    @Html.DropDownList(
        "", 
        triStateValues, 
        new { 
            @class = "list-box tri-state", 
            data_bind="value: " + x + ViewData.TemplateInfo.GetFullHtmlFieldName("") // or you could also use ViewData.ModelMetadata.PropertyName if you want to get only the property name and not the entire navigation hierarchy name
        }
    )
} 
else 
{
    @Html.CheckBox("", value ?? false, new { @class = "check-box" })
}

Note the lines:

var x = ViewData["koObservablePrefix"];
if ((x != "") && (x != null)) { x = x + "."; }

koObservablePrefix is there so that I can add an arbitrary prefix to my viewModel ko.observable. You could do other things if you so choose.

I use the variable x as follows:

data_bind="value: " + x + ViewData.TemplateInfo.GetFullHtmlFieldName("")

That way, if I don't pass in the additionalViewData "koObservablePrefix" it all still works.

So, now I can write:

@Html.EditorFor(model => model.IsClient, "koBoolEditorFor", new { koObservablePrefix = "Account" })

that will render as:

<select class="list-box tri-state" data-bind="value: Account.IsBank" id="IsBank" name="IsBank">

Note the "value: Account.IsBank" data-bind attribute value.

This is useful if, for example, your views strongly typed model is of type Account, but in your accountViewModel for your page, you have a more complex structure, so you need to package your observables in an account object. EG:

function account(accountId, personId, accountName, isClient, isProvider, isBank) {

    this.AccountId   = ko.observable(accountId);
    this.PersonId    = ko.observable(personId);
    this.AccountName = ko.observable(accountName);
    this.IsClient    = ko.observable(isClient);
    this.IsProvider  = ko.observable(isProvider);
    this.IsBank      = ko.observable(isBank);
}

function accountViewModel() { 

    var self = this;

    this.selectedCostCentre = ko.observable('');            
    this.Account = new account(@Model.AccountId, @Model.PersonId, '@Model.AccountName', '@Model.IsClient','@Model.IsProvider', '@Model.IsBank');
              // etc. etc
}

If you don't have this kind of structure, then, the code will pick up the structure. It is just a matter of tailoring your viewModel js to this, uhmmm, flexible convention.

Hope this isn't too confusing...

Community
  • 1
  • 1
awrigley
  • 13,481
  • 10
  • 83
  • 129