Code-BehindからXAMLリソースへの便利なアクセスを取得する方法





Code-BehindのXAMLリソースを使用するのが最も便利な方法を説明します。 この記事では、XAML名前空間の仕組みを理解し、 XmlnsDefinitionAttributeについて学び 、T4テンプレートを使用し、XAMLリソースにアクセスするための静的クラスを生成します。



はじめに



XAMLを使用する場合、 ResourceDictionaryは、スタイル、ブラシ、コンバーターなどのリソースを整理するために広く使用されます。 App.xamlで宣言されたリソースを検討してください。



<Application.Resources> <SolidColorBrush x:Key="HeaderBrush" Color="Black" /> <Application.Resources>
      
      





ビューのレイアウトでは、このリソースは次のように使用されます。



 <TextBlock x:Name="header" Foreground="{StaticResource HeaderBrush}" />
      
      





Code-Behindから同じリソースを使用する必要がある場合、通常は次の構成が使用されます。



 header.Foreground = (SolidColorBrush)Application.Current.Resources["HeaderBrush"];
      
      





これには多くの欠点があります。リソースの文字列識別子(キー)によりエラーが発生する可能性が高くなり、リソースが多い場合は、xamlにアクセスしてこのキーを覚える必要があります。 別の不快な些細なことは、SolidColorBrushにキャストすることです。 すべてのリソースはオブジェクトとして保存されます。



これらの欠点は、コード生成の助けを借りて取り除くことができ、最終的には次のような構成になります。



 header.Foreground = AppResources.HeaderBrush;
      
      





記事の目的はアプローチ自体を示すことなので、簡単にするために1つのApp.xamlファイルに焦点を当てますが、必要に応じて、簡単な変更によりプロジェクト内のすべてのXAMLリソースを処理し、それらを個別のファイルに分解することもできます。



T4テンプレートを作成します。







T4についてあまり詳しくない場合は、 この記事を読むことができます。



T4ヘッダーの標準を使用します。



 <#@ template debug="false" hostSpecific="true" language="C#" #> <#@ output extension=".cs" #>
      
      





T4テンプレートクラスが継承されるTextTransformationクラスのHostプロパティにアクセスするには、 hostSpecific = trueに設定する必要があります。 ホストは、プロジェクトファイル構造とその他の必要なデータにアクセスします。



すべてのリソースは、静的読み取り専用プロパティを使用して1つの静的クラスに収集されます。 テンプレートのメインスケルトンは次のようになります。



 using System.Windows; namespace <#=ProjectDefaultNamespace#> { public static class AppResources { <# foreach (var resource in ResourcesFromFile("/App.xaml")) { OutputPropery(resource); } #> } }
      
      





スクリプトに関連するすべての補助関数とプロパティは、スクリプトの本体の後にある<#+#>セクションで宣言されます。



VsProjectの最初のプロパティは、スクリプト自体が存在するソリューションからプロジェクトを選択します。



 private VSProject _vsProject; public VSProject VSProject { get { if (_vsProject == null) { var serviceProvider = (IServiceProvider) Host; var dte = (DTE)serviceProvider.GetService(typeof (DTE)); _vsProject = (VSProject)dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject.Object; } return _vsProject; } }
      
      





ProjectDefaultNamespace-プロジェクトの名前空間:



 private string _projectDefaultNamespace; public string ProjectDefaultNamespace { get { if (_projectDefaultNamespace == null) _projectDefaultNamespace = VSProject.Project.Properties.Item("DefaultNamespace").Value.ToString(); return _projectDefaultNamespace; } }
      
      





XAMLからリソースを収集する主な作業はすべて、 ResourcesFromFile(string filename)によって行われます 。 それがどのように機能するかを理解するために、ネームスペース、プレフィックス、およびそれらがXAMLでどのように使用されるかをより詳しく調べてみましょう。



XAMLの名前空間とプレフィックス



C#で特定の型を一意に指すには、型名とそれが宣言されている名前空間を完全に指定する必要があります。



 var control = new CustomNamespace.CustomControl();
      
      





usingを使用する場合、上記の構成はより短く書くことができます。



 using CustomNamespace; var control = new CustomControl();
      
      





XAMLの名前空間も同様に機能します。 XAMLはXMLのサブセットであり、XMLから名前空間を宣言するための規則を使用します。



XAML CustomControl型は次のように宣言されます。



 <local:CustomControl />
      
      





この場合、ドキュメントを分析するとき、XAMLアナライザーはローカルプレフィックスを調べます。 ローカルプレフィックスは、このタイプを探す場所を記述しています。



 xmlns:local="clr-namespace:CustomNamespace"
      
      





予約された属性名xmlnsは、これがXML名前空間宣言であることを示します。 プレフィックス名(この場合、「 local 」)は、XMLマークアップのルール内であれば何でもかまいません。 また、まったく存在しない場合もあります。名前空間宣言は次の形式を取ります。



 xmlns="clr-namespace:CustomNamespace"
      
      





このエントリは、プレフィックスなしで宣言されたアイテムのデフォルト名前空間を設定します。 たとえば、 CustomNamespace名前空間がデフォルトで宣言されている場合、接頭辞なしでCustomControlを使用できます。



 <CustomControl />
      
      





上記の例では、 xmlns属性値にはclr-namespaceラベルが含まれ、その直後に.net名前空間への参照が続きます。 これにより、XAMLアナライザーは、 CustomNamespace名前空間でCustomControlを検索する必要があることを理解します。



SolidColorBrushなど、SDKに含まれるタイプは、プレフィックスなしで宣言されます。



 <SolidColorBrush Color="Red" />
      
      





これは、デフォルトの名前空間がXAMLドキュメントのルート要素で宣言されているため可能です。



 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      
      





これは、XAMLで名前空間を宣言する2番目の方法です。 xmlns属性の値は一意のエイリアス文字列であり、 clr-namespaceは含まれていません。 XAMLパーサーは、このようなレコードを検出すると、プロジェクトアセンブリの.netでXmlnsDefinitionAttribute属性を確認します。



XmlnsDefinitionAttribute属性は、エイリアス文字列に対応する名前空間を記述するアセンブリを何度も変更します。



 [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Media")] [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Shapes")]
      
      





System.WIndowsアセンブリはこれらの属性の多くでマークされているため、エイリアスschemas.microsoft.com/winfx/2006/xaml/presentationには、System.Windows、System.Windows.Mediaなどの標準SDKからの多くの名前空間が含まれています。 。 これにより、XML名前空間を.netの複数の名前空間にマップできます。



同じエイリアスの下で結合された2つの名前空間に同じ名前の型がある場合、衝突が発生し、XAMLアナライザーは型の取得元を見つけられないことに注意してください。



そのため、XAML名前空間は、 clr-namespaceを使用する場合は1対1、エイリアスを使用する場合は1 対多の 2つの異なる方法で.netの名前空間にマップされることがわかりました。



xmlnsコンストラクトは通常、XAMLドキュメントのルート要素にありますが、実際にxmlnsが少なくとも以前と同じレベルで宣言されていれば十分です。 CustomControlの場合次のエントリが可能です。



 <local:CustomControl xmlns:local="clr-namespace:CustomNamespace" />
      
      





ReamurceDictionary XAMLマークアップを正しく理解できるスクリプトを作成するには、上記のすべてが必要です。これには、SDKに含まれる異種オブジェクトや、名前空間を宣言するために異なるメソッドを使用するサードパーティライブラリのコンポーネントが含まれる場合があります。



主要部分に取りかかりましょう



XAMLタグによって完全な型名を決定するタスクは、 ITypeResolverインターフェイスに割り当てられます。



 public interface ITypeResolver { string ResolveTypeFullName(string localTagName); }
      
      





名前空間宣言には2つのタイプがあるため、このインターフェイスには2つの実装があります。



 public class ExplicitNamespaceResolver : ITypeResolver { private string _singleNamespace; public ExplicitNamespaceResolver(string singleNamespace) { _singleNamespace = singleNamespace; } public string ResolveTypeFullName(string localTagName) { return _singleNamespace + "." + localTagName; } }
      
      





この実装は、clr-namespaceを使用して.net名前空間が明示的に指定されている場合を処理します。



別のケースはXmlnsAliasResolverによって回答されます



 public class XmlnsAliasResolver : ITypeResolver { private readonly List<Tuple<string, Assembly>> _registeredNamespaces = new List<Tuple<string, Assembly>>(); public XmlnsAliasResolver(VSProject project, string alias) { foreach (var reference in project.References.OfType<Reference>() .Where(r => r.Path.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))) { try { var assembly = Assembly.ReflectionOnlyLoadFrom(reference.Path); _registeredNamespaces.AddRange(assembly.GetCustomAttributesData() .Where(attr => attr.AttributeType.Name == "XmlnsDefinitionAttribute" && attr.ConstructorArguments[0].Value.Equals(alias)) .Select(attr => Tuple.Create(attr.ConstructorArguments[1].Value.ToString(), assembly))); } catch {} } } public string ResolveTypeFullName(string localTagName) { return _registeredNamespaces.Select(i => i.Item2.GetType(i.Item1 + "." + localTagName)).First(i => i != null).FullName; } }
      
      





XmlnsAliasResolverは、 XmlnsDefinitionAttribute属性でラベル付けされた名前空間を特定のエイリアスに登録し、それらが宣言されているアセンブリを登録します。 結果が見つかるまで、登録された各ネームスペースで検索が実行されます。



ResolveTypeFullNameの実装では、見つかったタイプのキャッシュをオプションで追加できます。



TypeResolversヘルパーメソッドは、XAMLドキュメントを解析し、すべての名前空間を見つけてXMLプレフィックスにマップします。出力は辞書Dictionary <string、ITypeResolver>です。



 public Dictionary<string, ITypeResolver> TypeResolvers(XmlDocument xmlDocument) { var resolvers = new Dictionary<string, ITypeResolver>(); var namespaces = xmlDocument.SelectNodes("//namespace::*").OfType<XmlNode>().Distinct().ToArray(); foreach (var nmsp in namespaces) { var match = Regex.Match(string.Format("{0}=\"{1}\"", nmsp.Name, nmsp.Value), @"xmlns:(?<prefix>\w*)=""((clr-namespace:(?<namespace>[\w.]*))|([^""]))*"""); var namespaceGroup = match.Groups["namespace"]; var prefix = match.Groups["prefix"].Value; if (string.IsNullOrEmpty(prefix)) prefix = ""; if (resolvers.ContainsKey(prefix)) continue; if (namespaceGroup != null && namespaceGroup.Success) { //  namespace resolvers.Add(prefix, new ExplicitNamespaceResolver(namespaceGroup.Value)); } else { //Alias    XmlnsDefinitionAttribute resolvers.Add(prefix, new XmlnsAliasResolver(VSProject, nmsp.Value)); } } return resolvers; }
      
      





xpathの使用- "// namespace :: *"は、ドキュメント内の任意のレベルで宣言されたすべてのネームスペースを選択します。 次に、各名前空間は、正規表現によって解析され、接頭辞と、 clr-namespaceの後に指定されている.net名前空間があれば、それが指定されます。 結果に応じて、 ExplicitNamespaceResolverまたはXmlnsAliasResolverが作成され、デフォルトのプレフィックスまたはプレフィックスにマップされます。



ResourcesFromFileメソッドはすべてをまとめます:



 public Resource[] ResourcesFromFile(string filename) { var xmlDocument = new XmlDocument(); xmlDocument.Load(Path.GetDirectoryName(VSProject.Project.FileName) + filename); var typeResolvers = TypeResolvers(xmlDocument); var nsmgr = new XmlNamespaceManager(xmlDocument.NameTable); nsmgr.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml"); var resourceNodes = xmlDocument.SelectNodes("//*[@x:Key]", nsmgr).OfType<XmlNode>().ToArray(); var result = new List<Resource>(); foreach (var resourceNode in resourceNodes) { var prefix = GetPrefix(resourceNode.Name); var localName = GetLocalName(resourceNode.Name); var resourceName = resourceNode.SelectSingleNode("./@x:Key", nsmgr).Value; result.Add(new Resource(resourceName, typeResolvers[prefix].ResolveTypeFullName(localName))); } return result.ToArray(); }
      
      







XAMLドキュメントをロードし、xpathのtypeResolversを初期化して適切に動作すると、 schemas.microsoft.com / winfx / 2006 / xaml 名前空間が XmlNamespaceManagerに追加されます 。これはResourceDictionaryのすべてのキー属性が指します。



xpathを使用する場合-属性キーを持つXAMLドキュメントオブジェクトのすべてのレベルから「// * [@ x:Key]」が選択されます。 次に、スクリプトは検出されたすべてのオブジェクトを実行し、 typeResolversの 「辞書」を使用して 、それぞれを.netタイプのフルネームに関連付けます。



出力は、コード生成に必要なすべてのデータを含むリソース構造の配列です。



 public struct Resource { public string Key { get; private set; } public string Type { get; private set; } public Resource(string key, string type) : this() { Key = key; Type = type; } }
      
      





そして最後に、結果のリソースをテキスト形式で表示するメソッド:



 public void OutputPropery(Resource resource) { #> private static bool _<#=resource.Key #>IsLoaded; private static <#=resource.Type #> _<#=resource.Key #>; public static <#=resource.Type #> <#=resource.Key #> { get { if (!_<#=resource.Key #>IsLoaded) { _<#=resource.Key #> = (<#=resource.Type #>)Application.Current.Resources["<#=resource.Key #>"]; _<#=resource.Key #>IsLoaded = true; } return _<#=resource.Key #>; } } <#+ }
      
      





KeyプロパティがXAMLからキー属性の値をそのまま返すことに注意してください。C#でプロパティを宣言するのに無効な文字を含むキーを使用すると、エラーが発生します。 すでに大きなコードを複雑にしないために、私はあなたの裁量でプロパティセーフな名前を取得する実装を意図的に残します。



おわりに



このスクリプトは、WPF、Silverlight、WindowsPhoneプロジェクトで機能します。 WindowsRTファミリのUniversalAppsについては、次の記事でXamlTypeInfo.g.csに突入し、 XmlnsDefinitionAttributeに代わるIXamlMetadataProviderについて説明し 、UniversalAppsでスクリプトを機能させます。



ネタバレの下で、完全なスクリプトコードを見つけ、プロジェクトにコピーして、喜んで使用できます。



完全なスクリプトコード
 <#@ template debug="false" hostSpecific="true" language="C#" #> <#@ assembly name="System.Windows" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Linq" #> <#@ assembly name="System.Core" #> <#@ assembly name="System.Xml" #> <#@ assembly name="System.Xml.Linq" #> <#@ assembly name="EnvDTE" #> <#@ assembly name="VSLangProj" #> <#@ import namespace="EnvDTE" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Reflection" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="VSLangProj" #> <#@ output extension=".cs" #> using System.Windows; namespace <#=ProjectDefaultNamespace#> { public static class AppResourcess { <# foreach (var resource in ResourcesFromFile("/App.xaml")) { OutputPropery(resource); } #> } } <#+ public void OutputPropery(Resource resource) { #> private static bool _<#=resource.Key #>IsLoaded; private static <#=resource.Type #> _<#=resource.Key #>; public static <#=resource.Type #> <#=resource.Key #> { get { if (!_<#=resource.Key #>IsLoaded) { _<#=resource.Key #> = (<#=resource.Type #>)Application.Current.Resources["<#=resource.Key #>"]; _<#=resource.Key #>IsLoaded = true; } return _<#=resource.Key #>; } } <#+ } private VSProject _vsProject; public VSProject VSProject { get { if (_vsProject == null) { var serviceProvider = (IServiceProvider) Host; var dte = (DTE)serviceProvider.GetService(typeof (DTE)); _vsProject = (VSProject)dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject.Object; } return _vsProject; } } private string _projectDefaultNamespace; public string ProjectDefaultNamespace { get { if (_projectDefaultNamespace == null) _projectDefaultNamespace = VSProject.Project.Properties.Item("DefaultNamespace").Value.ToString(); return _projectDefaultNamespace; } } public struct Resource { public string Key { get; private set; } public string Type { get; private set; } public Resource(string key, string type) : this() { Key = key; Type = type; } } public Resource[] ResourcesFromFile(string filename) { var xmlDocument = new XmlDocument(); xmlDocument.Load(Path.GetDirectoryName(VSProject.Project.FileName) + filename); var typeResolvers = TypeResolvers(xmlDocument); var nsmgr = new XmlNamespaceManager(xmlDocument.NameTable); nsmgr.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml"); var resourceNodes = xmlDocument.SelectNodes("//*[@x:Key]", nsmgr).OfType<XmlNode>().ToArray(); var result = new List<Resource>(); foreach (var resourceNode in resourceNodes) { var prefix = GetPrefix(resourceNode.Name); var localName = GetLocalName(resourceNode.Name); var resourceName = resourceNode.SelectSingleNode("./@x:Key", nsmgr).Value; result.Add(new Resource(resourceName, typeResolvers[prefix].ResolveTypeFullName(localName))); } return result.ToArray(); } public Dictionary<string, ITypeResolver> TypeResolvers(XmlDocument xmlDocument) { var resolvers = new Dictionary<string, ITypeResolver>(); var namespaces = xmlDocument.SelectNodes("//namespace::*").OfType<XmlNode>().Distinct().ToArray(); foreach (var nmsp in namespaces) { var match = Regex.Match(string.Format("{0}=\"{1}\"", nmsp.Name, nmsp.Value), @"xmlns:(?<prefix>\w*)=""((clr-namespace:(?<namespace>[\w.]*))|([^""]))*"""); var namespaceGroup = match.Groups["namespace"]; var prefix = match.Groups["prefix"].Value; if (string.IsNullOrEmpty(prefix)) prefix = ""; if (resolvers.ContainsKey(prefix)) continue; if (namespaceGroup != null && namespaceGroup.Success) { //  namespace resolvers.Add(prefix, new ExplicitNamespaceResolver(namespaceGroup.Value)); } else { //Alias    XmlnsDefinitionAttribute resolvers.Add(prefix, new XmlnsAliasResolver(VSProject, nmsp.Value)); } } return resolvers; } public interface ITypeResolver { string ResolveTypeFullName(string localTagName); } public class ExplicitNamespaceResolver : ITypeResolver { private string _singleNamespace; public ExplicitNamespaceResolver(string singleNamespace) { _singleNamespace = singleNamespace; } public string ResolveTypeFullName(string localTagName) { return _singleNamespace + "." + localTagName; } } public class XmlnsAliasResolver : ITypeResolver { private readonly List<Tuple<string, Assembly>> _registeredNamespaces = new List<Tuple<string, Assembly>>(); public XmlnsAliasResolver(VSProject project, string alias) { foreach (var reference in project.References.OfType<Reference>() .Where(r => r.Path.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))) { try { var assembly = Assembly.ReflectionOnlyLoadFrom(reference.Path); _registeredNamespaces.AddRange(assembly.GetCustomAttributesData() .Where(attr => attr.AttributeType.Name == "XmlnsDefinitionAttribute" && attr.ConstructorArguments[0].Value.Equals(alias)) .Select(attr => Tuple.Create(attr.ConstructorArguments[1].Value.ToString(), assembly))); } catch {} } } public string ResolveTypeFullName(string localTagName) { return _registeredNamespaces.Select(i => i.Item2.GetType(i.Item1 + "." + localTagName)).First(i => i != null).FullName; } } string GetPrefix(string xamlTag) { if (string.IsNullOrEmpty(xamlTag)) throw new ArgumentException("xamlTag is null or empty", "xamlTag"); var strings = xamlTag.Split(new[] {":"}, StringSplitOptions.RemoveEmptyEntries); if(strings.Length <2) return ""; return strings[0]; } string GetLocalName(string xamlTag) { if (string.IsNullOrEmpty(xamlTag)) throw new ArgumentException("xamlTag is null or empty", "xamlTag"); var strings = xamlTag.Split(new[] {":"}, StringSplitOptions.RemoveEmptyEntries); if(strings.Length <2) return xamlTag; return strings[1]; } #>
      
      








All Articles