如何创建返回集合的XAML标记扩展

时间:2011-11-28 21:15:38

标签: c# xaml collections markup-extensions

我正在使用XAML序列化作为对象图(在WPF / Silverlight之外),我正在尝试创建一个自定义标记扩展,允许使用对XAML中其他地方定义的集合的选定成员的引用来填充集合属性

这是一个简化的XAML代码段,演示了我的目标:

<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>

每个 Country 对象的 Languages 属性将填充 IEnumerable&lt; Language&gt; ,其中包含对语言<的引用/ i> LanguageSelector 中指定的对象,这是一个自定义标记扩展。

以下是我尝试创建将担任此角色的自定义标记扩展程序:

[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;
    }
}

实际上,这段代码几乎可以工作。只要引用的对象在引用它们的对象之前在XAML中声明, ProvideValue 方法就会正确地返回用引用的项填充的 IEnumerable&lt; Language&gt; 。这是有效的,因为对 Language 实例的反向引用由以下代码行解析:

var token = service.Resolve(item);

但是,如果XAML包含前向引用(因为语言对象是在 Country 对象之后声明的),它会中断,因为这需要修复令牌(显然)不能被转换为语言

if (token == null)
{
    var names = new[] { item };
    token = service.GetFixupToken(names, true);
}

作为一项实验,我尝试将返回的集合转换为 Collection&lt; object&gt; ,希望XAML以某种方式稍后解析令牌,但在反序列化期间会抛出无效的强制转换异常。

任何人都可以建议如何最好地使这项工作?

非常感谢, 添

2 个答案:

答案 0 :(得分:13)

这是一个完整且有效的项目,可以解决您的问题。首先,我建议您在[XamlSetMarkupExtension]课程中使用Country属性,但实际上您需要的只是XamlSchemaContext的正向名称解析。

虽然该功能的文档非常精简,但 实际上可以告诉 Xaml服务推迟目标元素,以下代码说明了如何实现。请注意,即使示例中的部分相反,您的所有语言名称也都会得到正确解析。

基本上,如果您需要一个无法解析的名称,请通过返回修正令牌来请求延迟。是的,正如德米特里所说,它对我们来说是不透明的,但这并不重要。当您致电GetFixupToken(...)时,您将指定所需的名称列表。当这些名称可用时,您的标记扩展名ProvideValue将在稍后再次调用。那时,它基本上是一个重建。

此处未显示您还应该检查Boolean上的IsFixupTokenAvailable属性IXamlNameResolver。如果以后真的要找到这些名称,那么这应该返回true。如果值为false并且您仍然有未解析的名称,那么您应该努力使操作失败,大概是因为Xaml中给出的名称最终无法解析。

有些人可能会好奇地注意到这个项目不是一个WPF应用程序,即它没有引用WPF库;您必须添加到此独立 ConsoleApplication 的唯一参考是System.Xaml。即使using(历史工件)有System.Windows.Markup语句,也是如此。在.NET 4.0中,XAML服务支持从WPF(和其他地方)移动到核心BCL库中。

恕我直言,这一变化使 XAML服务成为没人听说过的最棒的BCL功能。没有更好的基础来开发具有根本重新配置功能作为主要要求的大型系统级应用程序。这种“app”的一个例子是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
        }
    };
}

[编辑...]

由于我刚学习 XAML服务,我可能一直在思考它。下面是一个简单的解决方案,它允许您使用内置标记扩展x:Arrayx:Reference来建立您希望的任何引用 - 完全在XAML 中。

不知怎的,我没有意识到x:Reference不仅可以填充属性(因为它常见:{x:Reference some_name}),但它也可以自己作为XAML标记({{1} }})。在任何一种情况下,它都充当对文档中其他对象的代理引用。这允许您使用对其他XAML对象的引用填充<Reference Name="some_name" />,然后只需将该数组设置为您的属性的值。 XAML解析器根据需要自动解析转发引用。

x:Array

要试用它,这是一个完整的控制台应用程序,它实例化前面的XAML文件中的<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> 对象。和以前一样,添加对myClass的引用并更改上面的XAML的第一行以匹配您的程序集名称。

System.Xaml.dll

答案 1 :(得分:6)

您不能使用GetFixupToken方法,因为它们返回的内部类型只能由在默认XAML架构上下文下工作的现有XAML编写器处理。

但您可以改用以下方法:

[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; }
    }
}