0

I have a custom HtmlHelper where I am trying to get values of a generic type property. My ViewModel has properties of type ChangeRequestFormField. The relevant parts of my ViewModel, class/interface, and the html helper are shown below.

In my helper, I need to access the IsRequired and ValueHasChanged properties from my ViewModel properties. This is working fine for ChangeRequestFormField. But when I get to ChangeRequestFormField, I get a the following error:

Unable to cast object of type 'StaffChanges.Models.ChangeRequestFormField1[System.Nullable1[System.Boolean]]' to type 'StaffChanges.Models.IChangeRequestFormField`1[System.Object]'.

The error occurs at this line in the helper:

var isRequired = ((IChangeRequestFormField<object>)metadata.Model).IsRequired;

Maybe I'm approaching this wrong, but I need a way to access those properties in the helper, not knowing the type in the ChangeFormField until runtime.

ViewModel:

public class JobChangeModel
{
    public ChangeRequestFormField<string> Reason1 { get; set; }
    public ChangeRequestFormField<bool?> IsTransferEventNeeded { get; set; }
}


public class ChangeRequestFormField<T> : IChangeRequestFormField<T>
{
    public ChangeRequestFormField(string formFieldType, T fieldValue, T originalValue)
    {
        this.FieldValue = fieldValue;
        this.OriginalValue = originalValue;

        switch (formFieldType)
        {
            case FormFieldTypes.DoNotRender:
                this.RenderField = false;
                this.IsRequired = false;
                break;

            case FormFieldTypes.Required:
                this.RenderField = true;
                this.IsRequired = true;
                break;

            case FormFieldTypes.Optional:
                this.RenderField = true;
                this.IsRequired = false;
                break;

            default:
                this.RenderField = false;
                this.IsRequired = false;
                break;
        }
    }

    public T FieldValue { get; set; }

    public bool IsRequired { get; private set; }

    public T OriginalValue { get; set; }

    public string OriginalValueString
    {
        get
        {
            return this.OriginalValue == null ? string.Empty : this.OriginalValue.ToString();
        }
    }

    public bool ValueHasChanged
    {
        get
        {
            return !EqualityComparer<T>.Default.Equals(this.FieldValue, this.OriginalValue);
        }
    }
}


public interface IChangeRequestFormField<out T>
{
    bool IsRequired { get; }

    string OriginalValueString { get; }

    bool ValueHasChanged { get; }
}

public static MvcHtmlString LabelForChangeRequestFormField<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes)
{
    if (expression.Body.Type.GetGenericTypeDefinition() != typeof(ChangeRequestFormField<>))
    {
        return html.LabelFor(expression, htmlAttributes);
    }

    var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
    var isRequired = ((IChangeRequestFormField<object>)metadata.Model).IsRequired;
    var valueChanged = ((IChangeRequestFormField<object>)metadata.Model).ValueHasChanged;

    // other code left out
}

Based on your code it doesn't look like the IChangeRequestFormField interface needs to be generic. If you remove the type parameter T from the interface declaration you will be able to cast all derived generic classes to the non-generic interface.

public interface IChangeRequestFormField
{
    bool IsRequired { get; }
    string OriginalValueString { get; }
    bool ValueHasChanged { get; }
}

public class ChangeRequestFormField<T> : IChangeRequestFormField
{
    // ...
}

Then, you could use it like this:

var isRequired = ((IChangeRequestFormField)metadata.Model).IsRequired;

Things get more complicated if you need to generic types in the interface. Then you will need to be careful with how you implement the covariant or contravariant aspects of your interface to support your desired casting behavior. Take a look at this article on MSDN.

NOTE

In particular, the reason your covariant interface doesn't work is because there is a restriction that the covariant type must be a reference type. And since Nullable<T> is not a reference type the cast fails.

If you find that you really need the covariant behavior you could implement your own nullable reference type to wrap value types such as bool and int.

4

1 回答 1

1

根据您的代码,界面看起来IChangeRequestFormField不需要是通用的。如果您T从接口声明中删除类型参数,您将能够将所有派生的泛型类转换为非泛型接口。

public interface IChangeRequestFormField
{
    bool IsRequired { get; }
    string OriginalValueString { get; }
    bool ValueHasChanged { get; }
}

public class ChangeRequestFormField<T> : IChangeRequestFormField
{
    // ...
}

然后,您可以像这样使用它:

var isRequired = ((IChangeRequestFormField)metadata.Model).IsRequired;

如果您需要在接口中使用泛型类型,事情会变得更加复杂。然后,您将需要注意如何实现接口的协变或逆变方面以支持所需的转换行为。查看MSDN 上的这篇文章

笔记

特别是,协变接口不起作用的原因是协变类型必须是引用类型的限制。由于Nullable<T>不是引用类型,因此强制转换失败。

如果您发现您确实需要协变行为,您可以实现自己的可为空引用类型来包装值类型,例如boolint

于 2013-10-15T15:25:48.217 回答