I am looking into initializing members of generic types declared in XAML. This is targeting the (reduced) generics support in WPF 4 and future Silverlight. (I have tried the scenarios below using x:TypeArguments
and XamlReader.Load
in VS2010 Beta 2, but will use TestClassInt32 : TestClass<int> { }
for simplicity, as it has the same behavior as using the generic type directly, but is easier to test using compiled xaml today.)
Here are the test types I am using.
public class TestClass<T> {
[TypeConverter( typeof(StringListToItemsConverter) )]
public IEnumerable<T> Items { get; set; }
public TestProperty<T> Property { get; set; }
}
[TypeConverter( typeof(TestPropertyConverter) )]
public struct TestProperty<T> {
public TestProperty( T value ) : this() { Value = value; }
public T Value { get; }
}
Here is the example scenario.
<StackPanel>
<StackPanel.DataContext>
<test:TestClassInt32 Items="1,2,3" Property="6" />
</StackPanel.DataContext>
<TextBox Text="{Binding Property.Value}" />
<ItemsControl ItemsSource="{Binding Items}" />
</StackPanel>
When I hard-code typeof(int)
into the converters for this example, everything works fine, but that approach obviously does not work for TestClass<double>
or TestClass<DateTime>
. The problem is that the TypeConverter.ConvertFrom
method does not have access to the destination type, only the source type. (This was not a problem when TypeConverter
was created in .NET 1.0, because the destination type could not be parameterized, but is an unfortunate limitation now.)
Here are the approaches I have looked at to get around this problem:
- Make the type converter generic; e.g.
[TypeConverter( typeof(TestPropertyConverter<T>) )]
- .NET does not support type parameters in attributes
- Have the
TypeConverter
return an intermediate type that either implementsIConvertible.ToType
, or has aTypeConvert
that canConvertTo
the destination type - XAML parser only does one-step conversions: if the returned object cannot be assigned to the destination, it throws an exception
- Define a custom type descriptor that will return the appropriate converter based on the actual type
- WPF ignores custom type descriptors, and
TypeDescriptor
does not even exist in Silverlight
Here are the alternatives I have come up with for "working around" this issue:
- Require the destination type to be embedded in the string; e.g.
"sys:Double 1,2,3"
- In Silverlight, have to hard-code a fixed set of supported types (in WPF, can use the
IXamlTypeResolver
interface to get the actual type that"sys:Double"
corresponds to) - Write a custom markup extension that takes a type parameter; e.g.
"{List Type={x:Type sys:Double}, Values=1,2,3}"
- Silverlight does not support custom markup extensions (it also does not support the
x:Type
markup extension, but you could use the hard-coded approach from option 1) - Create a wrapper type that takes a type parameter and re-defines all of the generic members as
object
, forwarding all member accesses to the underlying 开发者_如何学Pythonstrongly-typed object - Possible, but makes for a very poor user experience (have to cast to get underlying generic object, have to still use hard-coded list for type parameter on Silverlight, have to cache member assignments until the type argument is assigned, etc, etc; generally loses most of the benefits of strong typing with generics)
- In Silverlight, have to hard-code a fixed set of supported types (in WPF, can use the
Would be happy to hear any other ideas for working around this issue today, or in WPF 4.
In .NET 4, there is an IServiceProvider
available through which you can get IDestinationTypeProvider
, and then you should be able to do what you need.
In .NET 3 or 4, the IProvideValueTarget
service can give you the targetObject and targetProperty. From the targetProperty (a PropertyInfo
, MethodInfo
-for attached, or a DependencyProperty
), you can get the type.
To get theIDestinationTypeProvider
service provider from within ConvertFrom
of a TypeConverter
for example:
public override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo culture,
object value )
{
var typeProvider =
(IDestinationTypeProvider)context.GetService( typeof( IDestinationTypeProvider ) );
Type targetType = typeProvider.GetDestinationType();
// ... do stuff
return base.ConvertFrom( context, culture, value );
}
Rob Relyea's answer works, but unfortunately relies on services provided by XAML and will thus not work for other ITypeDescriptorContext
's. This also implies you need to reference System.Xaml
. This is undesirable for type definitions in assemblies which should not reference System.Xaml
.
I therefore opted instead to apply a special TypeConverter
to my generic type which redirects its implementation to a converter which is loaded through TypeDescriptor
. This allows other assemblies (e.g. using XAML) to specify the type converter to be loaded at runtime using:
TypeDescriptor.AddAttributes(
typeof( SomeGenericType<> ),
new TypeConverterAttribute( typeof( SomeGenericTypeConverter ) ) );
I created a RedirectTypeConverter
base class for such converters. The key components of the code required to implement this:
protected RedirectTypeConverter( Type type )
{
_type = type;
}
// Other methods are implemented similarly.
public override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo culture,
object value )
{
InitializeConverter();
return _converter.ConvertFrom( context, culture, value );
}
public void InitializeConverter()
{
if ( _converter != null )
{
return;
}
_converter = TypeDescriptor.GetConverter( _type );
if ( _converter.GetType() == GetType() )
{
string message = string.Format(
"Conversion failed. Converter for {0} is missing in TypeDescriptor.", _type );
throw new InvalidOperationException( message );
}
}
A full writeup and extended discussion is available on my blog.
精彩评论