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
Here is a complete and working project that solves your issue. At first I was going to suggest using the
[XamlSetMarkupExtension]
attribute on yourCountry
class, but actually all you need is theXamlSchemaContext
's forward name resolution.Although the documentation for that feature is very thin on the ground, you can in fact tell Xaml Services to defer your target element, and the following code shows how. Note that all of your language names get properly resolved even though the sections from your example are reversed.
Basically, if you need a name that couldn't be resolved, you request deferral by returning a fixup token. Yes, as Dmitry mentions it's opaque to us, but that doesn't matter. When you call
GetFixupToken(...)
, you will specify a list of names that you need. Your markup extension—ProvideValue
, that is—will be called again later when those names have become available. At that point, it's basically a do-over.Not shown here is that you should also check the
Boolean
propertyIsFixupTokenAvailable
on theIXamlNameResolver
. If the names are truly to be found later, then this should returntrue
. If the value isfalse
and you still have unresolved names, then you should hard-fail the operation, presumably because the names given in the Xaml ultimately couldn't be resolved.Some might be curious to note that this project is not a WPF app, i.e., it references no WPF libraries; the only reference you must add to this standalone ConsoleApplication is
System.Xaml
. This is true even though there is ausing
statement forSystem.Windows.Markup
(a historical artifact). It was in .NET 4.0 that the XAML Services support was moved from WPF (and elsewhere) and into the core BCL libraries.IMHO, this change made XAML Services the greatest BCL feature that nobody's heard of. There's no better foundation for developing a large systems-level application that has radical reconfiguration capability as a primary requirement. An example of such an 'app' is WPF.
[edit...]
As I'm just learning XAML Services, I may have been overthinking it. Below is a simple solution which allows you to establish whatever references you desire--entirely in XAML--using just the built-in markup extensions
x:Array
andx:Reference
.Somehow I hadn't realized that not only can
x:Reference
populate an attribute (as it's commonly seen:{x:Reference some_name}
), but it can also stand as a XAML tag on its own (<Reference Name="some_name" />
). In either case it functions as a proxy reference to an object elsewhere in the document. This allows you to populate anx:Array
with references to other XAML objects and then simply set the array as the value for your property. The XAML parser(s) automatically resolve forward references as required.To try it out, here's a complete console app that instantiates the
myClass
object from the preceding XAML file. As before, add a reference toSystem.Xaml.dll
and change the first line of the XAML above to match your assembly name.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: