How to create a XAML markup extension that returns a collection
Asked Answered
P

2

15

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

Padauk answered 28/11, 2011 at 21:15 Comment(2)
+1 Thanks for posting this. I found it a great exercise for my learning curve on XAML Servces. I hope the suggestions I posted below might still of use to you a year later.Ierna
@Glenn Slayden: thanks for your follow-up on this. You have proposed two very innovative solutions. Although my code is now implemented and working using the idea suggested by DmitryG, it will be interesting to review it and adapt it to use your more concise approach.Padauk
F
8

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; }
    }
}
Flyback answered 29/11, 2011 at 8:26 Comment(4)
Thank you so much! This is a really smart solution.Padauk
Dmitry, see my answer and working solution on this page; it's no problem to use GetFixupToken (and requires no unsupported coding) but the technique certainly is not well-documented at all. The trick is that the token--while opaque to you--is built for you to contain the names that you need. What's not mentioned anywhere is that you then return the token from your ProvideValue method. This tells XAML Services to try back again later.Ierna
@GlennSlayden: Hi Glen, thank you for the alternative solution. The information you provided is very interest to me... (+1!!!)Flyback
@DmitryG: Thanks for the vote. But after I posted my answer I realized there's no need to use a custom markup extension at all! (I updated my answer) There sure is alot to learn in .NET XAML Services...Ierna
I
16

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;
        }
    };
}
Ierna answered 14/9, 2012 at 4:9 Comment(3)
This is a great answer - can I ask what your learning resources are for XAML Services? It's something I'm trying to get into myself but can't find much in the way of tutorials, only the MSDN documentation which can be quite denseRothstein
Good question; thinking back now, much of what I've learned about XAML is from endless hours spent in .NET Reflector and examining runtime stack traces. And one thing that definitely helped right at the outset was to create thin stub/proxy classes which subclass every single function of XamlType, XamlMember, etc. Fortunately, XAML services is very generous with these callbacks. My stub printed to the debug console every time XAML called me--with indenting--and showed the best places/times to insert your actual hooks.Ierna
BTW, a key thing to remember is that XAML can call you back out-of-order (with respect to the lexical order of a XAML source file), notably when a MarkupExtension requests a fixup token, which results in one or more additional passes until all XAML names are resolved.Ierna
F
8

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; }
    }
}
Flyback answered 29/11, 2011 at 8:26 Comment(4)
Thank you so much! This is a really smart solution.Padauk
Dmitry, see my answer and working solution on this page; it's no problem to use GetFixupToken (and requires no unsupported coding) but the technique certainly is not well-documented at all. The trick is that the token--while opaque to you--is built for you to contain the names that you need. What's not mentioned anywhere is that you then return the token from your ProvideValue method. This tells XAML Services to try back again later.Ierna
@GlennSlayden: Hi Glen, thank you for the alternative solution. The information you provided is very interest to me... (+1!!!)Flyback
@DmitryG: Thanks for the vote. But after I posted my answer I realized there's no need to use a custom markup extension at all! (I updated my answer) There sure is alot to learn in .NET XAML Services...Ierna

© 2022 - 2024 — McMap. All rights reserved.