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

Tim*_*ter 13 c# collections xaml 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>
Run Code Online (Sandbox Code Playgroud)

语言的每个属性国家目的是与被填充的IEnumerable <语言>含于引用语言中指定的对象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;
    }
}
Run Code Online (Sandbox Code Playgroud)

实际上,这段代码几乎可以工作.只要引用的对象在引用它们的对象之前在XAML中声明,ProvideValue方法就会正确返回用引用的项填充的IEnumerable <Language>.这是有效的,因为语言实例的后向引用由以下代码行解决:

var token = service.Resolve(item);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

非常感谢,蒂姆

Gle*_*den 13

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

尽管该功能的文档在实际上非常薄,但实际上您可以告诉Xaml Services推迟目标元素,以下代码显示了如何实现.请注意,即使示例中的部分相反,也可以正确解析所有语言名称.

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

这里没有显示的是,你还应该检查Boolean属性IsFixupTokenAvailableIXamlNameResolver.如果以后真的要找到这些名字,那么应该返回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
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

[编辑...]

由于我刚刚学习XAML服务,我可能一直在思考它.下面是一个简单的解决方案,它允许您建立所需的任何引用 - 完全在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>
Run Code Online (Sandbox Code Playgroud)

要试用它,这是一个完整的控制台应用程序,它实例化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;
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

  • 好问题; 现在回想一下,我所学到的关于XAML的大部分内容都来自.NET Reflector花费的无尽时间以及检查运行时堆栈跟踪.在开始时肯定有帮助的一件事是创建瘦的存根/代理类,它们是XamlType,XamlMember等的每个函数的子类.幸运的是,XAML服务对这些回调非常慷慨.每次XAML打电话给我时,我的存根打印到调试控制台 - 带缩进 - 并显示插入实际挂钩的最佳位置/时间. (3认同)

Dmi*_*ryG 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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 德米特里,在这个页面上看到我的答案和工作解决方案; 使用`GetFixupToken`(并且不需要不支持的编码)没有问题,但该技术当然没有完全记录.诀窍在于,令牌 - 虽然对你来说是不透明的 - 是为了包含你需要的名字而构建的.什么都没有提到,然后你从你的`ProvideValue`方法返回令牌*.这告诉XAML服务稍后再试. (2认同)