14

我正在将 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<Language>,其中包含对 LanguageSelector 中指定的 Language 对象的引用是一个自定义标记扩展。

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

[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<Language> 。这是可行的,因为对Language实例的向后引用由以下代码行解析:

var token = service.Resolve(item);

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

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

作为一个实验,我尝试将返回的集合转换为Collection<object>,希望 XAML 稍后会以某种方式解析标记,但它会在反序列化期间引发无效的强制转换异常。

谁能建议如何最好地让它工作?

非常感谢,蒂姆

4

2 回答 2

15

这是一个完整且有效的项目,可以解决您的问题。起初我打算建议[XamlSetMarkupExtension]在你的类上使用该属性Country,但实际上你所需要的只是XamlSchemaContext' 的前向名称解析。

尽管该功能的文档非常薄,但实际上您可以告诉Xaml 服务延迟您的目标元素,以下代码显示了如何执行此操作。请注意,即使您的示例中的部分被颠倒了,您的所有语言名称都会得到正确解析。

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

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

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

恕我直言,此更改使XAML 服务成为没人听说过的最伟大的 BCL 功能。开发具有彻底重新配置能力作为主要要求的大型系统级应用程序,没有比这更好的基础了。这种“应用程序”的一个例子是 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 Services,我可能想多了。下面是一个简单的解决方案,它允许您建立所需的任何引用——完全在 XAML中——仅使用内置标记扩展x:Arrayx:Reference.

不知何故,我没有意识到它不仅可以x:Reference填充属性(如常见的{x:Reference some_name}那样:),而且它本身也可以作为 XAML 标记(<Reference Name="some_name" />)。在任何一种情况下,它都充当对文档中其他位置的对象的代理引用。这允许您x:Array使用对其他 XAML 对象的引用来填充一个,然后只需将该数组设置为您的属性的值。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和以前一样,添加对上面 XAML 的第一行的引用并更改它以匹配您的程序集名称。

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;
        }
    };
}
于 2012-09-14T04:09:26.897 回答
7

不能使用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; }
    }
}
于 2011-11-29T08:26:04.867 回答