I am using XAML serialization for an object graph (outside of WPF / Silverlight) and I am trying to create a custom markup extension that will allow a collection property to be populated using references to selected members of a collection defined elsewhere in XAML.
Here's a simplified XAML snippet that demonstrates what I aim to achieve:
<myClass.Languages>
<LanguagesCollection>
<Language x:Name="English" />
<Language x:Name="French" />
<Language x:Name="Italian" />
</LanguagesCollection>
</myClass.Languages>
<myClass.Countries>
<CountryCollection>
<Country x:Name="UK" Languages="{LanguageSelector 'English'}" />
<Country x:Name="France" Languages="{LanguageSelector 'French'}" />
<Country x:Name="Italy" Languages="{LanguageSelector 'Italian'}" />
<Country x:Name="Switzerland" Languages="{LanguageSelector 'English, French, Italian'}" />
</CountryCollection>
</myClass.Countries>
The Languages property of each Country object is to be populated with an IEnumerable<Language> containing references to the Language objects specified in the LanguageSelector, which is a custom markup extension.
Here is my attempt at creating the custom markup extension that will serve in this role:
[ContentProperty("Items")]
[MarkupExtensionReturnType(typeof(IEnumerable<Language>))]
public class LanguageSelector : MarkupExtension
{
public LanguageSelector(string items)
{
Items = items;
}
[ConstructorArgument("items")]
public string Items { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var service = serviceProvider.GetService(typeof(IXamlNameResolver)) as IXamlNameResolver;
var result = new Collection<Language>();
foreach (var item in Items.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(item => item.Trim()))
{
var token = service.Resolve(item);
if (token == null)
{
var names = new[] { item };
token = service.GetFixupToken(names, true);
}
if (token is Language)
{
result.Add(token as Language);
}
}
return result;
}
}
In fact, this code almost works. As long as the referenced objects are declared in XAML before the objects that are referencing them, the ProvideValue method correctly returns an IEnumerable<Language> populated with the referenced items. This works because the backward references to the Language instances are resolved by the following code line:
var token = service.Resolve(item);
But, if the XAML contains forward references (because the Language objects are declared after the Country objects), it breaks because this requires fixup tokens which (obviously) cannot be cast to Language.
if (token == null)
{
var names = new[] { item };
token = service.GetFixupToken(names, true);
}
As an experiment I tried converting the returned collection to Collection<object> in the hope that XAML would somehow resolve the tokens later, but it throws invalid cast exceptions during deserialization.
Can anyone suggest how best to get this working?
Many thanks, Tim
You can't use the GetFixupToken methods because they return an internal type that can only be processed by the existing XAML writers that work under the default XAML schema context.
But you can use the following approach instead:
[ContentProperty("Items")]
[MarkupExtensionReturnType(typeof(IEnumerable<Language>))]
public class LanguageSelector : MarkupExtension {
public LanguageSelector(string items) {
Items = items;
}
[ConstructorArgument("items")]
public string Items { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) {
string[] items = Items.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
return new IEnumerableWrapper(items, serviceProvider);
}
class IEnumerableWrapper : IEnumerable<Language>, IEnumerator<Language> {
string[] items;
IServiceProvider serviceProvider;
public IEnumerableWrapper(string[] items, IServiceProvider serviceProvider) {
this.items = items;
this.serviceProvider = serviceProvider;
}
public IEnumerator<Language> GetEnumerator() {
return this;
}
int position = -1;
public Language Current {
get {
string name = items[position];
// TODO use any possible methods to resolve object by name
var rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider
var nameScope = NameScope.GetNameScope(rootProvider.RootObject as DependencyObject);
return nameScope.FindName(name) as Language;
}
}
public void Dispose() {
Reset();
}
public bool MoveNext() {
return ++position < items.Length;
}
public void Reset() {
position = -1;
}
object IEnumerator.Current { get { return Current; } }
IEnumerator IEnumerable.GetEnumerator() { return this; }
}
}