【问题标题】:How to create a XAML markup extension that returns a collection如何创建返回集合的 XAML 标记扩展
【发布时间】:2012-01-08 07:11:29
【问题描述】:

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

这是一个简化的 XAML sn-p,展示了我的目标:

<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 中指定的对象,这是一个自定义标记扩展。

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

[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 实例的向后引用由以下代码行解析:

var token = service.Resolve(item);

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

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

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

谁能建议如何最好地完成这项工作?

非常感谢, 蒂姆

【问题讨论】:

  • +1 感谢您发布此信息。我发现它是我学习 XAML 服务曲线的一个很好的练习。我希望我在下面发布的建议在一年后仍然对您有用。
  • @Glenn Slayden:感谢您对此的跟进。您提出了两个非常创新的解决方案。虽然我的代码现在已经实现并使用 DmitryG 建议的想法运行,但审查它并对其进行调整以使用您更简洁的方法将会很有趣。

标签: c# xaml collections markup-extensions


【解决方案1】:

您不能使用 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; }
    }
}

【讨论】:

  • 非常感谢!这是一个非常聪明的解决方案。
  • Dmitry,在此页面上查看我的答案和工作解决方案;使用GetFixupToken 没有问题(并且不需要不受支持的编码),但该技术当然根本没有详细记录。诀窍在于,令牌(虽然对您不透明)是为您构建的,以包含您需要的名称。任何地方都没有提到的是,然后您从您的ProvideValue 方法中返回令牌。这会告诉 XAML 服务稍后再试。
  • @GlennSlayden:您好 Glen,感谢您提供替代解决方案。你提供的信息让我很感兴趣……(+1!!!)
  • @DmitryG:感谢您的投票。但是在我发布我的答案后,我意识到根本不需要使用自定义标记扩展! (我更新了我的答案)在 .NET XAML 服务中肯定有很多东西要学...
【解决方案2】:

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

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

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

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

有些人可能会好奇地注意到这个项目不是一个 WPF 应用程序,即它不引用 WPF 库;您必须添加到此独立 ConsoleApplication 的唯一引用是 System.Xaml。即使System.Windows.Markup 有一个using 声明(历史文物)也是如此。正是在 .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 服务,我可能想多了。下面是一个简单的解决方案,它允许您建立您想要的任何引用——完全在 XAML 中——仅使用内置标记扩展 x:Arrayx:Reference

不知何故,我没有意识到x:Reference 不仅可以填充属性(正如它常见的:{x:Reference some_name}),而且它也可以作为 XAML 标记本身(&lt;Reference Name="some_name" /&gt;)。在任何一种情况下,它都充当对文档中其他位置的对象的代理引用。这允许您使用对其他 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>

要试用它,这里有一个完整的控制台应用程序,它从前面的 XAML 文件中实例化 myClass 对象。和以前一样,添加对 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;
        }
    };
}

【讨论】:

  • 这是一个很好的答案 - 我可以问一下您的 XAML 服务学习资源是什么吗?这是我正在尝试深入了解的内容,但在教程中找不到太多内容,只有 MSDN 文档可能非常密集
  • 好问题;现在回想起来,我学到的有关 XAML 的大部分内容来自于在 .NET Reflector 中花费的无数时间和检查运行时堆栈跟踪。一开始肯定有帮助的一件事是创建瘦存根/代理类,这些类将 XamlType、XamlMember 等的每个函数子类化。幸运的是,XAML 服务对这些回调非常慷慨。每次 XAML 调用我时,我的存根都会打印到调试控制台(带有缩进),并显示插入实际钩子的最佳位置/时间。
  • 顺便说一句,要记住的关键是 XAML 可以回调 out-of-order(关于 a XAML 源文件),特别是当 MarkupExtension 请求修复令牌时,这会导致一个或多个额外的传递,直到所有 XAML 名称都被解析。
猜你喜欢
  • 1970-01-01
  • 2012-02-01
  • 2016-08-13
  • 1970-01-01
  • 2013-08-25
  • 2011-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多