Matt Lengenfelder
Matt Lengenfelder

Reputation: 739

merge HtmlAttributes in asp.net core

I've been using Chris Pratt's MergeHtmlAttributes Html Helper Extension method for a while in my asp.net mvc 5 editor templates. I started the process of switching over an application to Asp.net core 1.1(.net framework 4.5.2). And The htmlhelperExtension isn't working for me.

public static partial class HtmlHelperExtensions
{
    //https://cpratt.co/html-editorfor-and-htmlattributes/
    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
            {
                if(item.Key?.ToString() == "divClass")
                {
                    continue;
                }
                defaultHtmlAttributes[item.Key] = item.Value;
            }
        }

        return defaultHtmlAttributes;
    }
}

When I copy the class over it flags the statment: using System.Web.Mvc; -Cannot resolve symbol MVC. And after removing that using statement I get the message cannot resolve symbol "HtmlHelper" in MergeHtmlAttributes(this HtmlHelper helper, ...) I have the option of adding either Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper or .HtmlHelper<Tmodel> I chose .HtmlHelper. After that it refers to the line RouteValueDictionary htmlAttributes = and says it cannot convert IDictionary<string, object> to system.web.Routing.RouteValueDictionary. And I should change the type to IDictionary<string, object> or cast to RouteValueDictionary. Either way I get the following error when trying to use MergeHtmlAttributes in one of my editor templates.

'IHtmlHelper<object>' does not contain a definition for 'MergeHtmlAttributes' and the best extension method overload 'HtmlHelperExtensions.MergeHtmlAttributes(HtmlHelper, object, object)' requires a receiver of type 'HtmlHelper'

This line is throwing the error-> var htmlAttributes = Html.MergeHtmlAttributes(ViewData, defaultHtmlAttributesObject);

Is there a way to get this to work in asp.net core or is there a different method to achieve the same results? Here's an example of one of my editor templates so you can see the MergeHtmlAttributes being used. If I can't build the template like this anymore is there a newer/better way to do it using tag helpers? I really like having the labelfor, txtboxfor, ValidationMessageFor, etc all in one html helper.

@model int?

@{
    var defaultHtmlAttributesObject = new { @class = "form-control" };
    var htmlAttributes = Html.MergeHtmlAttributes(ViewData, defaultHtmlAttributesObject);

    object divClass;
    ViewData.TryGetValue("divClass", out divClass);
    if (divClass == null) { divClass = ""; }

    IDictionary<string, object> validationAttributes = Html.GetUnobtrusiveValidationAttributes("");
    Html.ViewContext.FormContext.RenderedField(ViewData.TemplateInfo.GetFullHtmlFieldName(null), false);
}

<div class="form-group @divClass @(Html.ValidationErrorFor(x => x, " has-error"))">
    @Html.LabelFor(x => x, new { @class = "control-label" })
    @if (validationAttributes.ContainsKey("data-val-required"))
    {<span class="text-danger">*</span>}
    @Html.TextBoxFor(x => x, htmlAttributes)
    @Html.ValidationMessageFor(model => model, "", new { @class = "text-danger" })
</div>

F.Y.I. while converting to asp.net core 1.1 (and .net framework 4.5.2) I ended up putting the connection string in it's app.config file which allowed EF6 to work with Asp.net core so I can keep using the EF code I had built, for whatever reason it wouldn't find the connection string in appsettings.json.

Upvotes: 3

Views: 2523

Answers (1)

Matt Lengenfelder
Matt Lengenfelder

Reputation: 739

Initially I didn't realize Microsoft was using IHtmlHelper now instead of HtmlHelper.

Once I modified the code to reflect that change and found Microsoft's mvc repository on github so that I was able to find their implementation of AnonymousObjectToHtmlAttributes it all fell together.

The below code is working for me, but I might have to make some changes for edge cases I haven't yet thought of.

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

using Microsoft.AspNetCore.Mvc.Rendering;

public static class HtmlHelperExtensions
{
    //http://cpratt.co/html-editorfor-and-htmlattributes/
    /// <summary>
    /// This is used with the EditorTemplates to copy the page element's htmlAttributes over to the editorTemplates 
    /// while still being able to specify values to always use (like @class = "form-control") in the editorTemplates.
    /// </summary>
    public static IDictionary<string, object> MergeHtmlAttributes(this IHtmlHelper helper, object htmlAttributesObject, object defaultHtmlAttributesObject)
    {
        var concatKeys = new[] { "class" };

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

        RouteValueDictionary htmlAttributes = new RouteValueDictionary(htmlAttributesDict != null
            ? htmlAttributesDict
            : AnonymousObjectToHtmlAttributes(htmlAttributesObject));

        RouteValueDictionary defaultHtmlAttributes = new RouteValueDictionary(defaultHtmlAttributesDict != null
            ? defaultHtmlAttributesDict
            : 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
            {
                if(item.Key == "divClass")
                {
                    continue;
                }
                defaultHtmlAttributes[item.Key] = item.Value;
            }
        }

        return defaultHtmlAttributes;
    }

    private static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
    {
        var dictionary = htmlAttributes as IDictionary<string, object>;
        if (dictionary != null)
        {
            return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
        }

        dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

        if (htmlAttributes != null)
        {
            var stringAttributes = htmlAttributes.ToString();
            stringAttributes = stringAttributes.Replace("{", "").Replace("}", "");
            string[] attributesArray = stringAttributes.Split(new[] { ','}, StringSplitOptions.RemoveEmptyEntries);

            foreach (var helper in attributesArray)
            {
                string[] attribKeyValue = helper.Trim().Split(' ');
                dictionary[attribKeyValue.First()] = attribKeyValue.Last();
            }
        }

        return dictionary;
    }
}

Upvotes: 3

Related Questions