开发者

EditorFor() and additionalViewData: how to add data in helper class?

EditorFor() can take an object additionalViewData parameter which the typical method to populate is something like:

EditorFor(model => model.PropertyName, new { myKey = "myValue" })

How can I i开发者_开发知识库nspect the contents of additionalViewData, add or append to existing value for a key, etc in a custom HTML Helper?

I've tried these approaches:

  • convert to Dictionary<string, object>() and add/append values: doesn't work as it looks like the implementation of EditorFor in MVC uses new RouteValueDictionary(additionalViewData) which embeds the dictionary within a dictionary
  • convert to RouteValueDictionary using new RouteValueDictionary(additionalViewData) but that has same (or very similar) issue as above

I'm also open to "you're doing it wrong" -- maybe I'm missing a simpler approach. Keep in mind what I'm trying to do is write an HTML helper that is reusable and adds some values to the additionalViewData to be used by custom views. Some of the values depend on metadata from the property so it is not quite so easy as just use a bunch of different templates.

Update with example of what I'm doing:

    public static MvcHtmlString myNullableBooleanFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> choice, string templateName, object additionalViewData)
    {            
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(choice, htmlHelper.ViewData);

        /*
    here need to add to additionalViewData some key values among them:
    { propertyName, metadata.PropertyName }

     */

        StringBuilder sb = new StringBuilder();
        sb.AppendLine(htmlHelper.EditorFor(choice, templateName, additionalViewData).ToString());
        MvcHtmlString validation = htmlHelper.ValidationMessageFor(choice);
        if (validation != null)
            sb.AppendLine(validation.ToString());
        return new MvcHtmlString(sb.ToString());
    }

Update with what happens when I convert the anonymous object to a Dictionary<string, object>() and pass that dictionary to EditorFor():

I put a break point in the Razor view and examined ViewData. It appears that the dictionary passed into EditorFor() is put inside another Dictionary<string, object>(). In the "Immediate Window", ViewData looks like this:

ViewData.Keys
Count = 4
    [0]: "Comparer"
    [1]: "Count"
    [2]: "Keys"
    [3]: "Values"

See how the dictionary has the contents of a dictionary within it? Yes, the actual data is in that inner dictionary however unsurprisingly, this doesn't work.

Added bounty.


in my case I have an editor for a boolean field that I want to be a yes/no radio. I use the additionalViewData property to set the text of yes/no to be localized. Seems to work great for me!

Here is the code for my custom editorFor:

@model bool?       
@{
    var yes = ViewBag.TrueText ?? @Resources.WebSiteLocalizations.Yes;
    var no = ViewBag.FalseText ?? @Resources.WebSiteLocalizations.No;
}
<div class="title">
    @Html.LabelFor(model => model)
    @Html.RequiredFor(model => model)
</div>
<div class="editor-field">
    @Html.RadioButton("", "True", (Model.HasValue && Model.Value))@yes
    <br />
    @Html.RadioButton("", "False", (Model.HasValue && Model.Value == false))@no
    <br />
    @Html.ValidationMessageFor(model => model)
</div>
@Html.DescriptionFor(model => model)   

Here is how you call this custom EditorFor:

@Html.EditorFor(model => model.IsActive, new {TrueText = @Resources.WebSiteLocalizations.Active, FalseText = @Resources.WebSiteLocalizations.InActive})


If I understand correctly, you are trying to iterate over properties of an anonymous type. If so: How do I iterate over the properties of an anonymous object in C#?

[Edit] Ok, it's more than that. This is really bothering me now because I love C# and it won't let me do what Python does, which I also love. So here's a solution, if you are using C# 4 but it's messy and will work for a similar problem I have but maybe not exactly for you:

    // Assume you've created a class to hold commonly used additional view data
    // called MyAdditionalViewData. I would create an inheritance hierarchy to
    // contain situation-specific (area-specific, in my case) additionalViewData.
class MyAdditionalViewData
{
    public string Name { get; set; }

    public string Sound { get; set; }
}     

/* In your views */
// Pass an instance of this class in the call to your HTML Helper
EditorFor(model => model.PropertyName, 
    new { adv = new MyAdditionalViewData { Name = "Cow", Sound = "Moo" } }) ;               

    /* In your HTML helper */
    // Here's the C# 4 part that makes this work.
dynamic bovine = new ExpandoObject();

// Copy values
var adv = x.adv;
if (adv.Name != null) bovine.Name = adv.Name;
if (adv.Sound != null) bovine.Sound = adv.Sound;

// Additional stuff
bovine.Food = "Grass";
bovine.Contents = "Burgers";

    // When the MVC framework converts this to a route value dictionary
    // you'll get a proper object, not a dictionary within a dictionary
var dict = new RouteValueDictionary(bovine);

There has got to be a better way.


Have you tried just adding the data to

helper.ViewData[xxx] = yyy;

The ViewData collection is a global collection for the ViewContext so I think just adding it to the global ViewData will make it available when the EditorTemplate is rendered out.

MORE INFO: As I understand it, the additionalViewdata property is just an easy way to add a collection/anything to the global ViewData on the fly after you've decided which control to render. Still uses the same contextual collection and is not so much a different object as a late and clean way to add to the same context dictionary.

I haven't tried this yet, so if I'm missing the point, say so and I'll just remove the answer.


The RouteValueDictionary uses the TypeDescriptor infrastructure (TypeDescriptor.GetProperties(obj)) to reflect the additionalViewData object. As this infrastructure is extensible, you can create a class that implements the ICustomTypeDescriptor and provides fake properties of your choice, e.g. based on an internal dictionary. In the following article you will find an implementation: http://social.msdn.microsoft.com/Forums/en/wpf/thread/027cb000-996e-46eb-9ebf-9c41b07d5daa Having this implementation you can easily add "properties" with arbitrary name and value and pass it as the addtionalViewData object. To get the existing properties from the initial object you can use the same method as MVC will do later, call the TypeDescriptor.GetProperties, enumerate the properties and get the name and value of the property, and add them to your new "object" as well.


A bit late, but there is a working solution using CodeDom available here.

public interface IObjectExtender
{
    object Extend(object obj1, object obj2);
}

public class ObjectExtender : IObjectExtender
{
    private readonly IDictionary<Tuple<Type, Type>, Assembly>
        _cache = new Dictionary<Tuple<Type, Type>, Assembly>();

    public object Extend(object obj1, object obj2)
    {
        if (obj1 == null) return obj2;
        if (obj2 == null) return obj1;

        var obj1Type = obj1.GetType();
        var obj2Type = obj2.GetType();

        var values = obj1Type.GetProperties()
            .ToDictionary(
                property => property.Name,
                property => property.GetValue(obj1, null));

        foreach (var property in obj2Type.GetProperties()
            .Where(pi => !values.ContainsKey(pi.Name)))
            values.Add(property.Name, property.GetValue(obj2, null));

        // check for cached
        var key = Tuple.Create(obj1Type, obj2Type);
        if (!_cache.ContainsKey(key))
        {
            // create assembly containing merged type
            var codeProvider = new CSharpCodeProvider();
            var code = new StringBuilder();

            code.Append("public class mergedType{ \n");
            foreach (var propertyName in values.Keys)
            {
                // use object for property type, avoids assembly references
                code.Append(
                    string.Format(
                        "public object @{0}{{ get; set;}}\n",
                        propertyName));
            }
            code.Append("}");

            var compilerResults = codeProvider.CompileAssemblyFromSource(
                new CompilerParameters
                    {
                        CompilerOptions = "/optimize /t:library",
                        GenerateInMemory = true
                    },
                code.ToString());

            _cache.Add(key, compilerResults.CompiledAssembly);
        }

        var merged = _cache[key].CreateInstance("mergedType");
        Debug.Assert(merged != null, "merged != null");

        // copy data
        foreach (var propertyInfo in merged.GetType().GetProperties())
        {
            propertyInfo.SetValue(
                merged,
                values[propertyInfo.Name],
                null);
        }

        return merged;
    }
}

Usage:

var merged = Extender.Extend(new { @class }, additionalViewData));

Thanks to the original author, Anthony Johnston!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜