views:

1119

answers:

4

Using the templated helpers in MVC2.0 I ran into a dillema, how to get the items to fill a dropdownlist. I am using a [UIHint(BadgesDropDown)] attribute, but how will i get the list items without violating the MVC Pattern, should the controller place them in the ViewData? Should the BadgesDropDown.ascx invoke a Helper to get them ?

Right now i am going for:

BadgesDropDown.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.DropDownList("", ViewData["Badges"] as IEnumerable<SelectListItem>)%>

Controller

ViewData["Badges"] = new SelectList(SiteRepository.GetBadges(), "RowKey", "BadgeName");

Is this the way to go ?

A: 

I would love to know this as well.

Daoming Yang
I would be glad if someone at least pointed a link to a concrete case. This must be a recurrent pattern.
Carlos Fernandes
A: 

I implemented the solution as the above example. One thing to be noted is that Helpers should only work with the data supplied to them, see View dependency

The best practice is to write Html helpers unaware of controllers and contexts. They should do their job only based on what data is supplied by the caller.

I agree on the above statement. It's just that a lot of work needs to be done when compared to the regular ASP.Net development.

Carlos Fernandes
A: 

In the MVC 2 a great new method... which if used relies on all the attribute data.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<glossaryDB.EntityClasses.AssociationEntity>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Association: Edit
</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="MainContent" runat="server">
    <h3>Association: Edit</h3>
    <% using (Html.BeginForm()) { %>
        <fieldset style="padding: 1em; margin: 0; border: solid 1px #999;">
            <%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
            <%= Html.EditorForModel() %>
            <input type="submit" value="  Submit  " />
        </fieldset>
    <% } %>
    <p><%= Html.ActionLink("Details", "Index") %></p>
</asp:Content>

For this to work there is 2 options. Either the UIHint has to provide the source of the data or the controller must. If the UIHint does then the data provided to thhe dropdown is fixed. The other option is the controller, which allows us to switch out the dropdown data with a different set of data as reqired.

There is some related examples I found:

Nerd Dinner

[1]: searcch for codeclimber.net.nz and how-to-create-a-dropdownlist-with-asp.net-mvc [2]: bradwilson.typepad.com and templates-part-5-master-page-templates

Quentin J S
+1  A: 

There's been a lot of discussion about this topic recently. Similar roadblocks are encountered with dates, date ranges and multi-select checkbox lists. Anywhere you might want to use a rich set of html controls. I've been experimenting with the concept of child ViewModels and I think the solution is cleaner than other approaches I've tried.

The basic concept is that you define a small view model that's closely coupled to a custom EditorTemplate.

In your example, we would start with a (child) ViewModel that's specific to a single select list:

public class SelectModel
{
  #region SelectModel(string value, IEnumerable<SelectListItem> items)
  public SelectModel(string value, IEnumerable<SelectListItem> items)
  {
    _value = value;
    Items = new List<SelectListItem>(items);

    _Select();
  } 
  #endregion

  // Properties

  public List<SelectListItem> Items { get; private set; }

  public string Value
  { 
    get { return _value; }
    set { _value = value; _Select();}
  }
  private string _value;

  // Methods

  private void _Select()
  {
    Items.ForEach(x => x.Selected = (Value != null && x.Value == Value));
  }
}

In the view model that wants to use the dropdown you compose the select model (we're all using view models, right?):

public class EmailModel
{
  // Constructors

  public EmailModel()
  {
    Priority = new SelectModel("normal", _ToPrioritySelectItems());
  }

  // Properties

  public SelectModel Priority { get; set; }

  // Methods

  private IEnumerable<SelectListItem> _ToPrioritySelectItems()
  {
    List<SelectListItem> result = new List<SelectListItem>();

    result.Add(new SelectListItem() { Text = "High", Value = "high" });
    ...
  }

Note this is a simple example with a fixed set of dropdown items. If they are coming from the domain layer, the controller passes them into the ViewModel.

Then add an editor template SelectModel.ascx in Shared/EditorTemplates

<%@ Control Inherits="System.Web.Mvc.ViewUserControl<SelectModel>" %>

<div class="set">
  <%= Html.LabelFor(model => model) %>
  <select id="<%= ViewData.ModelMetadata.PropertyName %>_Value" name="<%=ViewData.ModelMetadata.PropertyName %>.Value">
  <% foreach (var item in Model.Items) { %>
    <%= Html.OptionFor(item) %>
  <% } %>
  </select>
</div>

Note: OptionFor is a custom extension that does the obvious

The trick here is that the id and name are set using the compound format that the default ModelBinder expects. In our example "Priority.Value". So the string based Value property that is defined as part of SelectModel is set directly. The setter takes care of updating the list of Items to set the default select option if we need to redisplay the form.

Where this "child view model" approach really shines is more complex "control snippets of markup". I now have child view models that follow a similar approach for MultiSelect lists, Start/End date ranges, and Date + time combinations.

As soon as you go down this path, the next obvious question becomes validation.

I ended up having all of my child ViewModel's implement a standard interface:

public interface IValidatable
{
  bool HasValue { get; }
  bool IsValid { get; }
}

Then, I have a custom ValidationAttribute:

public class IsValidAttribute : ValidationAttribute
{
  // Constructors

  public IsValidAttribute()
  {
    ErrorMessage = "(not valid)";
  }

  // Properties

  public bool IsRequired { get; set; }

  // Methods

  private bool Is(object value)
  {
    return value != null && !"".Equals(value);
  }

  public override bool IsValid(object value)
  {
    if (!Is(value) && !IsRequired)
      return true;

    if (!(value is IValidatable))
      throw new InvalidOperationException("IsValidAttribute requires underlying property to implement IValidatable");

    IValidatable validatable = value as IValidatable;
    return validatable.IsValid;
  }
}

Now you can just put attributes on properties that are child ViewModel based like any scalar property:

[IsValid(ErrorMessage = "Please enter a valid start date/time")]
public DateAndTimeModel Start { get; set; }
Rob