DmitryEfimenko / TwitterBootstrapMvc

Fluent implementation of ASP.NET-MVC HTML helpers for Twitter Bootstrap.
Apache License 2.0
224 stars 79 forks source link

DropDownListFor with different label text #48

Closed ipostanogov closed 11 years ago

ipostanogov commented 11 years ago

I have some classes

class Foo 
{
    public int id;
    public int barId;
    public virtual Bar Bar;
}

class Bar 
{
    public int id;
    [Display(Name = "BarDispName")]
    public string name;
}

public class FooMap : EntityTypeConfiguration<Foo>
{
    public FooMap()
    {
        // Primary Key
        this.HasKey(t => t.id);

        // Relationships
        this.HasRequired(t => t.Bar)
            .WithMany(t => t.Foos)
            .HasForeignKey(d => d.barId);
    }
}

The closes thing I need is

@Html.Bootstrap().ControlGroup().DropDownListFor(m => m.barId, ViewBag.barId as IEnumerable<SelectListItem>)

The problem is that generated label is 'bar Id*', but I want it to be the same as

@Html.LabelFor(m => m.Bar.name)

plus '*' if needed.

So I suggest to add an overload to DropDownListFor with two expressions:

If there is a way to do it now, I would be glad to know about it.

DmitryEfimenko commented 11 years ago

I believe the functionality you are looking for is achieved through extension method .Label():

@(Html.Bootstrap().ControlGroup().DropDownListFor(m => m.barId, ViewBag.barId as IEnumerable<SelectListItem>)
    .Label().LabelText(Model.Bar.name).ShowRequiredStar(true))

Let me know if this works out for you.

ipostanogov commented 11 years ago

'Model.Bar.name' returns field's value of the sent object, not the 'LabelFor'.

Now I have

<div class="control-group">
    @Html.LabelForRequired(m => m.Bar.name)
    <div class="controls">
        @Html.DropDownList("barId", String.Empty)
    </div>
</div>

with LabelForRequired helper, but for me it seems костыль :)

DmitryEfimenko commented 11 years ago

Right... now I see what you are trying to do. Though I can't think of a prod case where such functionality would be needed. Just out of curiosity, what classes Foo and Bar actually are? I want to understand why you can't slap [Display(Name = "BarDispName")] on public int id; of the Foo class.

Also keep in mind that in your case rendered label will not be linked to the dropdown because it will have incorrect for attribute.

ipostanogov commented 11 years ago

E.g. Foo is item, Bar is maker. Every item has maker, every maker has name.

While adding item, the maker should be selected from list of existing makers.

DropDownListFor(m => m.barId, ViewBag.barId as IEnumerable<SelectListItem>)

almost suites me, except that the label is bad. Yes, I know that I could do

[Display(Name = "Maker")]
public int barId { get; set; }

but it violates DRY principle, because

[Display(Name = "Maker")]
public string name { get; set; }

attribute is already presented in Bar class.

And what if I would need to display other field in other view, e.g. maker's address. What should be the barId's DisplayName?

Thanks for telling about label.for. For now it doesn't matter but I haven't noticed it.

DmitryEfimenko commented 11 years ago

Well, you can always use a CustomControls of ControlGroup:

@(Html.Bootstrap().ControlGroup()
    .CustomControls(Html.Bootstrap().DropDownListFor(m => m.barId, ViewBag.barId as IEnumerable<SelectListItem>))
    .LabelFor((m => m.Bar.name)))

.LabelFor will try to determine whether to slap a required star on label or not, but you can always override what it should be with extension method .ShowRequiredStar()

Let me know if this works for you!

ipostanogov commented 11 years ago

It seems almost OK, except that it doesn't select corresponding value of the Model.Bar.name in CustomControl. What is the easiest way to auto set it?

DmitryEfimenko commented 11 years ago

There is an extension method .SelectedValue() on drop DropDownList.

ipostanogov commented 11 years ago

Selected value - first, but not model's in the

@(Html.Bootstrap().ControlGroup().CustomControls(Html.Bootstrap().DropDownListFor(m => m.makerId, ViewBag.makerId as IEnumerable<SelectListItem>).SelectedValue(Model.Maker.name)).LabelFor((m => m.Maker.name)))

which is the same as in

@(Html.Bootstrap().ControlGroup().CustomControls(Html.Bootstrap().DropDownListFor(m => m.makerId, ViewBag.makerId as IEnumerable<SelectListItem>).SelectedValue(Model.makerId)).LabelFor((m => m.Maker.name)))

Now I use this obvious workaround

public static class MySelectExtensions
{
    public static MvcHtmlString DropDownListBootstrap<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel, TValue>> expression, string name, string optionLabel = "")
    {
        var contGroup = new TagBuilder("div");
        contGroup.AddCssClass("control-group");
        var controls = new TagBuilder("div");
        controls.AddCssClass("controls");
        controls.InnerHtml = htmlHelper.DropDownList(name, optionLabel).ToString();
        contGroup.InnerHtml = htmlHelper.LabelForRequired(expression) + controls.ToString();
        return MvcHtmlString.Create(contGroup.ToString());
    }
}

with this sample usage

@Html.DropDownListBootstrap(m => m.Maker.name, "makerId")

Not very elegant and absolutely ignores your framework, but it works. Still don't know your alternative.

Any ideas about making normal label.for attribute? Or it should be constructed directly?

DmitryEfimenko commented 11 years ago

I still do not quite understand the whole thing that you are trying to do. The whole thing feels to be a bit wrong if you need to write so much customization. Though I don't care that much :)

The verdict is that if you can construct this with the use of regular html helpers, you should be able to achieve the same with BMVC's custom control.

Based on the code you showed me I tried to reproduce the same logic with BMVC, but found that BMVC .DropDownList method does not have overload that you need. The one that does not require List<SelectListItem> and takes optionLabel. This is a bug and it will be fixed in upcoming release (which should happen today or tomorrow).

When it's fixed you would be able to achieve the same by writing this:

@(Html.Bootstrap().ControlGroup()
   .CustomControls(Html.Bootstrap().DropDownList("makerId", ""))
   .LabelFor(m => m.Maker.name))

As for the label's for attribute, you can use htmlAttributes to set it:

// BMVC:
.LabelFor(m => m.Maker.name).HtmlAttributes(new { @For = "makerId" })

// Regular helper:
@Html.LabelFor(m => m.Maker.name, new { @For = "makerId" })

Important: Notice uppercase "F" in the "For". It is needed to override default "for" value.