Setting TAB Order on MVC View with DropDownListFor

2019-09-21 00:04发布

Visually speaking, on one of my MVC Views I have about 20 fields in standard vertical order, with the first 8 or so having optional [Create] boxes in same <div> group over on the right.

My default tab order currently hits my first dropdown, then goes right to [Create], down to the next, then right, etc. What I would like to do set the TAB order to where it goes straight down my various fields and leave the [Create] boxes as optional for the user (or at the end of the tab order). While there seems to be a lot of discussion on this with a quick search, there seems to be inconsistent answers; a lot of them seemingly from a couple years ago regarding setting TAB Order in an EditorFor() but being forced to use Custom Editor Templates or switch to TextBoxFor()?

Hoping someone can weigh in on this. The below somewhat details my fields:

(8 of these DropDownListFor()):
@Html.DropDownListFor(model => model.STATUS_ID, (SelectList)ViewBag.Model_List, htmlAttributes: new { @class = "form-control dropdown", @id = "selectStatus" })

(12 of these EditorFor()):
@Html.EditorFor(model => model.NOTE, new { htmlAttributes = new { @class = "form-control" } })

1条回答
做自己的国王
2楼-- · 2019-09-21 00:20

To set the tab order, all you need to do is be able to add an extra attribute, tabindex to the generated field. That's easy enough with something like TextBoxFor or DropDownListFor, since they actually take an htmlAttributes parameter specifically for this purpose:

@Html.TextBoxFor(m => m.Foo, new { tabindex = 1 })

In the past, the same could not be said for EditorFor. Since it's a "templated" helper, the editor template, not the method call, effects what's generated. You can see this in the definition of EditorFor, as there's no htmlAttributes param like the other helpers have, but rather additionalViewData.

Starting with MVC 5.1, Microsoft made it possible to pass additional HTML attributes to EditorFor, via a specially named ViewData key, "htmlAttributes". As a result, you can achieve the same thing as when using something like TextBoxFor, although it's a little more verbose:

@Html.EditorFor(m => m.Foo, new { htmlAttributes = new { tabindex = 1 } })

See, you're still actually passing additionalViewData here, but that additional view data contains an anonymous object keyed to htmlAttributes. The built-in editor templates, then, know how to utilize ViewData["htmlAttributes"] to add additional attributes to the generated element. However, this only applies to the default editor templates because Microsoft has specifically programmed them to use this. As soon as you add your own custom editor templates, you're right back to where you started.

There's a number of ways you could approach this with custom editor templates. First, you could just pass the tab index directly as view data, and utilize that in your template:

@Html.EditorFor(m => m.Foo, new { tabindex = 1 })

Then, in your editor template:

@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { tabindex = ViewData["tabindex"]})

Second, you could mimic EditorFor's behavior with the default templates:

@Html.EditorFor(m => m.Foo, new { htmlAttributes = new { tabindex = 1 } })

Then, in your editor template:

@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, ViewData["htmlAttributes"])

However, that option doesn't allow you to have "default" attributes. It's an all or nothing approach. To truly be able to utilize ViewData["htmlAttributes"] as the built-in editor templates do, you'll need to combine the default attributes with the passed in ones, first, and then pass the whole shebang to htmlAttributes. I've got a blog post that discusses that in depth, but TL;DR: you'll need the following extension:

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;

public static partial class HtmlHelperExtensions
{
    public static IDictionary<string, object> MergeHtmlAttributes(this HtmlHelper helper, object htmlAttributesObject, object defaultHtmlAttributesObject)
    {
        var concatKeys = new string[] { "class" };

        var htmlAttributesDict = htmlAttributesObject as IDictionary<string, object>;
        var defaultHtmlAttributesDict = defaultHtmlAttributesObject as IDictionary<string, object>;

        RouteValueDictionary htmlAttributes = (htmlAttributesDict != null)
            ? new RouteValueDictionary(htmlAttributesDict)
            : HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject);
        RouteValueDictionary defaultHtmlAttributes = (defaultHtmlAttributesDict != null)
            ? new RouteValueDictionary(defaultHtmlAttributesDict)
            : HtmlHelper.AnonymousObjectToHtmlAttributes(defaultHtmlAttributesObject);

        foreach (var item in htmlAttributes)
        {
            if (concatKeys.Contains(item.Key))
            {
                defaultHtmlAttributes[item.Key] = (defaultHtmlAttributes[item.Key] != null)
                    ? string.Format("{0} {1}", defaultHtmlAttributes[item.Key], item.Value)
                    : item.Value;
            }
            else
            {
                defaultHtmlAttributes[item.Key] = item.Value;
            }
        }

        return defaultHtmlAttributes;
    }
}

And then you'll need to add the following to top of your custom editor templates:

@{
    var defaultHtmlAttributesObject = new { type = "date", @class = "form-control" };
    var htmlAttributesObject = ViewData["htmlAttributes"] ?? new { };
    var htmlAttributes = Html.MergeHtmlAttributes(htmlAttributesObject, defaultHtmlAttributesObject);
}

You'd change the defaultHtmlAttributesObject variable depending on what attributes the generated input should have by default for that particular template.

查看更多
登录 后发表回答