Here is a complete and working project that solves your issue. At first I was going to suggest using the [XamlSetMarkupExtension]
attribute on your Country
class, but actually all you need is the XamlSchemaContext
'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
property IsFixupTokenAvailable
on the IXamlNameResolver
. If the names are truly to be found later, then this should return true
. If the value is false
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 a using
statement for System.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.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows.Markup;
using System.Xaml;
namespace test
{
public class Language { }
public class Country { public IEnumerable<Language> Languages { get; set; } }
public class LanguageSelector : MarkupExtension
{
public LanguageSelector(String items) { this.items = items; }
String items;
public override Object ProvideValue(IServiceProvider ctx)
{
var xnr = ctx.GetService(typeof(IXamlNameResolver)) as IXamlNameResolver;
var tmp = items.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s_lang => new
{
s_lang,
lang = xnr.Resolve(s_lang) as Language
});
var err = tmp.Where(a => a.lang == null).Select(a => a.s_lang);
return err.Any() ?
xnr.GetFixupToken(err) :
tmp.Select(a => a.lang).ToList();
}
};
public class myClass
{
Collection<Language> _l = new Collection<Language>();
public Collection<Language> Languages { get { return _l; } }
Collection<Country> _c = new Collection<Country>();
public Collection<Country> Countries { get { return _c; } }
// you must set the name of your assembly here ---v
const string s_xaml = @"
<myClass xmlns=""clr-namespace:test;assembly=ConsoleApplication2""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
<myClass.Countries>
<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'}"" />
</myClass.Countries>
<myClass.Languages>
<Language x:Name=""English"" />
<Language x:Name=""French"" />
<Language x:Name=""Italian"" />
</myClass.Languages>
</myClass>
";
static void Main(string[] args)
{
var xxr = new XamlXmlReader(new StringReader(s_xaml));
var xow = new XamlObjectWriter(new XamlSchemaContext());
XamlServices.Transform(xxr, xow);
myClass mc = (myClass)xow.Result; /// works with forward references in Xaml
}
};
}
[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
and x: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 an x: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.
<myClass xmlns="clr-namespace:test;assembly=ConsoleApplication2"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<myClass.Countries>
<Country x:Name="UK">
<Country.Languages>
<x:Array Type="Language">
<x:Reference Name="English" />
</x:Array>
</Country.Languages>
</Country>
<Country x:Name="France">
<Country.Languages>
<x:Array Type="Language">
<x:Reference Name="French" />
</x:Array>
</Country.Languages>
</Country>
<Country x:Name="Italy">
<Country.Languages>
<x:Array Type="Language">
<x:Reference Name="Italian" />
</x:Array>
</Country.Languages>
</Country>
<Country x:Name="Switzerland">
<Country.Languages>
<x:Array Type="Language">
<x:Reference Name="English" />
<x:Reference Name="French" />
<x:Reference Name="Italian" />
</x:Array>
</Country.Languages>
</Country>
</myClass.Countries>
<myClass.Languages>
<Language x:Name="English" />
<Language x:Name="French" />
<Language x:Name="Italian" />
</myClass.Languages>
</myClass>
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 to System.Xaml.dll
and change the first line of the XAML above to match your assembly name.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Xaml;
namespace test
{
public class Language { }
public class Country { public IEnumerable<Language> Languages { get; set; } }
public class myClass
{
Collection<Language> _l = new Collection<Language>();
public Collection<Language> Languages { get { return _l; } }
Collection<Country> _c = new Collection<Country>();
public Collection<Country> Countries { get { return _c; } }
static void Main()
{
var xxr = new XamlXmlReader(new StreamReader("XMLFile1.xml"));
var xow = new XamlObjectWriter(new XamlSchemaContext());
XamlServices.Transform(xxr, xow);
myClass mc = (myClass)xow.Result;
}
};
}