Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I change the input element name attribute value in a razor view model using a custom attribute in a model?

I have the following:

@model Pharma.ViewModels.SearchBoxViewModel
<div class="smart-search">
    @using (Html.BeginForm("Index", "Search", FormMethod.Get, new { @class = "form-horizontal", role = "form" }))
    {
        <div class="form-group">
            <div class="hidden-xs- col-sm-1 col-md-1 col-lg-1 text-right">
                @Html.LabelFor(m => m.SearchPhrase, new { @class = "control-label" })
            </div>
            <div class="col-xs-8 col-sm-8 col-md-9 col-lg-10">
                @Html.TextBoxFor(m => m.SearchPhrase, new { @class = "form-control" })
            </div>
            <div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
                <input type="submit" value="Search" class="btn btn-default" />
            </div>
        </div>
    }
</div>

As you can see this is creating an input element.

The view model passed to the view contains the following:

public class SearchBoxViewModel
{
    [Required]
    [Display(Name = "Search")]
    public string SearchPhrase { get; set; }
}

At the moment the input element contains a name attribute with the value of "SearchPhrase" but I would like the value to be just "q" without renaming the property.

I would prefer an extension which allows me to call TextBoxFor but without the need of having to supply the Name property, so that the custom attribute somehow sets the value of the Name property automatically to the value specified in the custom attribute.

The following is an example of what I mean:

public class SearchBoxViewModel
{
    [Required]
    [Display(Name = "Search")]
    [Input(Name = "q")]
    public string SearchPhrase { get; set; }
}

Combined with:

@model Pharma.ViewModels.SearchBoxViewModel
<div class="smart-search">
    @using (Html.BeginForm("Index", "Search", FormMethod.Get, new { @class = "form-horizontal", role = "form" }))
    {
        <div class="form-group">
            <div class="hidden-xs- col-sm-1 col-md-1 col-lg-1 text-right">
                @Html.LabelFor(m => m.SearchPhrase, new { @class = "control-label" })
            </div>
            <div class="col-xs-8 col-sm-8 col-md-9 col-lg-10">
                @Html.TextBoxFor(m => m.SearchPhrase, new { @class = "form-control" })
            </div>
            <div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
                <input type="submit" value="Search" class="btn btn-default" />
            </div>
        </div>
    }
</div>

Which would then produce something similar to the following:

<div class="smart-search">
    <form action="/Search/Index" method="get" class="form-horizontal" role="form">
        <div class="form-group">
            <div class="hidden-xs- col-sm-1 col-md-1 col-lg-1 text-right">
                <label for="Search" class="control-label">Search</label>
            </div>
            <div class="col-xs-8 col-sm-8 col-md-9 col-lg-10">
                <input type="text" name="q" id="Search" value="" class="form-control" />
            </div>
            <div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
                <input type="submit" value="Search" class="btn btn-default" />
            </div>
        </div>
    </form>
</div>

I would like this custom attribute to take effect whenever the SearchBoxViewModel is used regardless of what template is used to prevent errors, with the intention of being clear to programmers, while creating a user-friendly query string for the user.

Is it possible to do this using a custom attribute on the SearchPhrase property in a similar fashion to how the display name is changed?

like image 506
Professor of programming Avatar asked Jun 10 '15 15:06

Professor of programming


People also ask

What is the file extension for using Razor view with C# syntax?

The Razor syntax consists of Razor markup, C#, and HTML. Files containing Razor generally have a . cshtml file extension.

How would you use the input tag helper to bind an input element to an expression in a Razor view file?

The Input Tag Helper binds an HTML <input> element to a model expression in your razor view. The Input Tag Helper: Generates the id and name HTML attributes for the expression name specified in the asp-for attribute. asp-for="Property1.

What is the difference between Razor view and Razor page?

The difference between them is that View Pages are Razor views that are used to provide the HTML representations (aka views) for services in much the same way View Pages work for MVC Controllers.


2 Answers

I wrote something simple but can be a start to write the complete solution.

First I wrote a simple Attribute with the name you provided:

public class InputAttribute : Attribute
{
    public string Name { get; set; }
}

Then I wrote a Html helper that wraps default TextBoxFor and searches for Input attribute and if any, It will replace name attribute of generated HtmlString from TextBoxFor:

public static MvcHtmlString MyTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
    var memberExpression = expression.Body as MemberExpression;

    var attr = memberExpression.Member.GetCustomAttribute(typeof (InputAttribute)) as InputAttribute;
    var result = htmlHelper.TextBoxFor(expression, htmlAttributes);
    if (attr != null)
    {
        var resultStr = result.ToString();
        var match = Regex.Match(resultStr, "name=\\\"\\w+\\\"");
        return new MvcHtmlString(resultStr.Replace(match.Value, "name=\"" + attr.Name + "\""));
    }

    return result;
}

Then use this html helper in razor views:

@Html.MyTextBoxFor(m => m.SearchPhrase, new { @class = "form-control" })

Also your model is as follows:

public class SearchBoxViewModel
{
    [Required]
    [Display(Name = "Search")]
    [Input(Name = "q")]
    public string SearchPhrase { get; set; }
}

This is a way to complete solution:

  1. You have to implement all of the overloads of TextBoxFor
  2. If you try to send form data to an action with parameter of type SearchBoxViewModel you will get a 404 because ModelBinder can not bind request parameters to this ViewModel. So you will have to write a ModelBinder to solve this problem.
  3. You have to write LabelFor accordingly to match for attribute correctly.

EDIT: In case of your problem you don't have to deal with case 2 because you send a GET request and you will get the form parameters in query string. So you may write your action signature like:

public ActionResult Search(string q)
{
  // use q to search
}

The problem occurs when you have a non-primitive type in your action parameters. In this case ModelBinder tries to match query string items (Or request payload) with properties of type of action parameter. For example:

public ActionResult Search(SearchBoxViewModel vm)
{
  // ...
}

In this case, query string (or request payload) has your search query in a parameter named q (because name of input is q and html form sends request in form of key-values consist of input name and input value). So MVC can not bind q to SearchPhrase in LoginViewModel and you will get a 404.

like image 61
alisabzevari Avatar answered Nov 15 '22 20:11

alisabzevari


I know this isn't what you are explicitly asking for but I feel that having a different ViewModel name from the actual form name undermines one of the core conventions in MVC and may be misleading.

As an alternative, you can just add a new property to the VM that mirrors SearchPhrase and has the proper name:

public class SearchBoxViewModel
{
    [Required]
    [Display(Name = "Search")]
    public string SearchPhrase { get; set; }

    [Display(Name = "Search")]
    public string q
    {
        get { return SearchPhrase; }
        set { SearchPhrase = value; }
    }
}

Change your view to use these:

@model Pharma.ViewModels.SearchBoxViewModel
<div class="smart-search">
    @using (Html.BeginForm("Index", "Search", FormMethod.Get, new { @class = "form-horizontal", role = "form" }))
    {
        <div class="form-group">
            <div class="hidden-xs- col-sm-1 col-md-1 col-lg-1 text-right">
                @Html.LabelFor(m => m.q, new { @class = "control-label" })
            </div>
            <div class="col-xs-8 col-sm-8 col-md-9 col-lg-10">
                @Html.TextBoxFor(m => m.q, new { @class = "form-control" })
            </div>
            <div class="col-xs-4 col-sm-3 col-md-2 col-lg-1">
                <input type="submit" value="Search" class="btn btn-default" />
            </div>
        </div>
    }
</div>

This would allow you to keep your code in the back end referencing SearchPhrase instead of q to make it easier on the programmers. Hopefully this view isn't spread out everywhere and you only have a single EditorTemplate or partial.

like image 34
Brad C Avatar answered Nov 15 '22 21:11

Brad C