WPFのコンバーターを簡素化する

私は約1年間WPFを使用してきましたが、その中のいくつかは率直に言って楽しいものです。 そのようなものの1つはコンバーターです。 くしゃみをするために、プロジェクトの腸のどこかで疑わしいインターフェースの実装を宣言し、突然必要になったらCtrl + Fで名前を探します。 マルチコンバーターでは、悪魔自身が混乱します。



状況はMVVMによって悪化しているため、この科学の奇跡を使用しないのはかなりまれです。 さて、コンバータの作成と使用のルーチンを少し楽にする時が来ました。



BooleanToVisibilityConverterなどの頻繁に使用されるコンバーターに反対しないように、すぐに予約してください 。それらは多くの場所で記憶され、再利用できます。 しかし、コンバータには非常に具体的なものが必要になることがよくあります。どういうわけか、私はそれからコンポーネント全体を作りたくありません。 そして、これは長い間、グローバルスコープを詰まらせ、このすべてのゴミに必要なものを見つけることは困難です。



コンバーターは、バインディングを操作するときに使用され、値を一方的または双方向に変換できるようにします(バインディングモードに応じて)。 コンバーターには、1つの値と多くの値の2つのタイプもあります。 IValueConverterおよびIMul​​tiValueConverterインターフェイスは、それぞれそれらに責任があります。



単一の値では、通常XAMLに組み込まれたBindingBaseマークアップ拡張機能を使用して、通常のバインダーを使用します。



<TextBlock Text="{Binding IntProp, Converter={StaticResource conv:IntToStringConverter}, ConverterParameter=plusOne}" />
      
      





複数値の場合、次の巨大な構成が使用されます。

 <TextBlock> <TextBlock.Text> <MultiBinding Converter="{StaticResource conv:IntToStringConverter}" ConverterParameter="plusOne"> <Binding Path="IntProp" /> <Binding Path="StringProp" /> </MultiBinding> </TextBlock.Text> </TextBlock>
      
      





コンバーター自体は次のようになります(同時に同じクラスに2つのコンバーターがありますが、別々に実行できます)。



 public class IntToStringConverter : IValueConverter, IMultiValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => (string)parameter == "plusOne" ? ((int)value + 1).ToString() : value.ToString(); public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) => $"{Convert(values[0], targetType, parameter, culture) as string} {values[1]}"; public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException(); }
      
      





例は合成であるため、これは非常に短いですが、0.5リットルがなくても、どんな種類の配列、どの型変換、一部のtargetTypeとカルチャ、実装なしのどのようなConvertBackかは明らかではありません。



これを簡素化するためのアイデアがいくつかあります。



  1. 単純な計算のためにxamlで直接c#コードの形のコンバーター;
  2. コードビハインドのメソッド参照の形式のコンバーター。変換の非常に特殊/特殊なケースがある場合、つまり、この変換を他の場所で再利用する意味がない場合。
  3. 標準の実装と同じ形式のコンバーター。ただし、それほど見栄えがよくないため、新しいコンバーターを作成するたびに、Googleにアクセスしてコンバーターの実装例を探す必要はありません。


パラグラフ1の実装方法についてお話したいと思う方はすぐに取り除いてください。 これは、ネットワーク上にいくつか実装されています。たとえば、 こちらです。 式ツリーのオプションも見ましたが、もう少し見えます。 このようなことは、算術演算と論理演算を扱うための最も単純な場合にのみ適しています。 そこでいくつかのクラスを呼び出したり、文字列を使用したりする必要がある場合、xml内のエスケープの問題と名前空間を含める問題が発生します。 ただし、最も単純なケースでは、そのようなものを使用できます。



ただし、2点と3点をさらに詳しく検討します。 分離コードに変換するメソッドを定義する必要があるとします。 どのように見えますか? 私はこのようなものだと思う:



 private string ConvertIntToString(int intValue, string options) => options == "plusOne" ? (intValue + 1).ToString() : intValue.ToString(); private string ConvertIntAndStringToString(int intValue, string stringValue, string options) => $"{ConvertIntToString(intValue, options)} {stringValue}";
      
      





これを前のオプションと比較してください。 少ないコード-より明確に。 別個に再利用されたコンバーターを持つバリアントは、次のようになります。



 public static class ConvertLib { public static string IntToString(int intValue, string options) => options == "plusOne" ? (intValue + 1).ToString() : intValue.ToString(); public static string IntAndStringToString(int intValue, string stringValue, string options) => $"{IntToString(intValue, options)} {stringValue}"; }
      
      





悪くないでしょ? まあ、しかし、彼はコンバーターの標準インターフェースのみを理解しているので、これをxamlと友達にする方法は? もちろん、すでに美しいメソッドを使用している標準のIValueConverter / IMul​​tiValueConverterの形式で、そのような各クラスのラッパーを作成できますが、読み取り可能なコンバーターごとにラッパーを宣言する必要がある場合、すべての意味が失われます。 1つの解決策は、次のようなラッパーをユニバーサルにすることです。



 public class GenericConverter : IValueConverter, IMultiValueConverter { public GenericConverter(/*         - */) { //    } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { //    ,    } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { //    ,    } public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { //    ,    } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { //    ,    } }
      
      





これはすべて理論上です。デリゲートを実際にコンバーターに転送する方法、およびXAMLのみでデリゲートを取得する方法を教えてください。



マークアップ拡張メカニズムMarkupExtensionが役立ちます。 MarkupExtensionクラスを継承してProvideValueメソッドを再定義するだけで十分です。XAMLでは 、バインディングのような式を中括弧で記述できますが、独自の動作メカニズムを使用できます。



マークアップ拡張機能を介して変換メソッドへのリンクを渡すための最も簡単なことは、それらの文字列名を使用することです。 分離コードメソッドは単にメソッドの名前で定義され、外部ライブラリの静的メソッドはClrNamespace.ClassName.MethodNameのようになり 、最後にポイントが存在することで区別できることに同意します(クラスの場合、少なくとも1つのポイントはクラスの名前とメソッドの間にありますグローバル名前空間にあります)。



見つけ出されたメソッドを識別する方法、デリゲートの形式でマークアップ拡張でそれらを取得してコンバータに渡す方法 マークアップ拡張機能( MarkupExtension )には、次のようなオーバーライド用のProvideValueメソッドがあります。



 public class GenericConvExtension : MarkupExtension { public override object ProvideValue(IServiceProvider serviceProvider) { // -  } }
      
      





オーバーライド可能なメソッドは、XAMLマークアップの値がこのマークアップ拡張を定義するプロパティに最終的に割り当てられるものを返す必要があります。 このメソッドは任意の値を返すことができますが、このマークアップ拡張機能をバインディング(またはマルチバインディング)のConverterプロパティに代入するため、戻り値はコンバーター、つまりIValueConverter / IMul​​tiValueConverterのインスタンスでなければなりません。 繰り返しますが、異なるコンバーターを作成しても意味がありません。1つのクラスを作成し、これら2つのインターフェイスを一度に実装して、コンバーターが単一および複数のバインダーの両方に適合するようにすることができます。



コードビハインドからの関数名またはコンバーターが呼び出す静的ライブラリを定義するマークアップ拡張機能に行を渡すには、 MarkupExtensionインスタンスにパブリック文字列プロパティを定義する必要があります。



 public string FunctionName { get; set; }
      
      





その後、マークアップで次のように記述できます。



 <TextBlock Text="{Binding IntProp, Converter={conv:GenericConvExtension FunctionName='ConvertIntToString'}, ConverterParameter=plusOne}" />
      
      





ただし、これは単純化できます。まず、XAMLのconv:GenericConvExtension拡張クラスの名前でExtensionを記述する必要はありません。conv:GenericConvで十分です。 さらに拡張機能では、関数の名前を使用してプロパティの名前を明示的に指定しないように、コンストラクターを定義できます。



 public GenericConvExtension(string functionName) { FunctionName = functionName; }
      
      





XAMLの式がさらにシンプルになりました。



 <TextBlock Text="{Binding IntProp, Converter={conv:GenericConv ConvertIntToString}, ConverterParameter=plusOne}" />
      
      





また、変換関数の名前に引用符がないことに注意してください。 文字列にスペースやその他の不健康な文字がない場合、一重引用符はオプションです。



ProvideValueメソッドでメソッドリンクを取得し、コンバーターのインスタンスを作成してこのリンクを渡すだけです。 メソッドへの参照はReflectionメカニズムを介して取得できますが、このためには、このメソッドが宣言されているランタイムタイプを知る必要があります。 静的クラスの変換メソッドの実装の場合、静的メソッドのフルネーム(クラスのフルネームを示す)が送信されるため、この文字列を解析し、タイプのフルネームを使用してReflectionを介してタイプを取得し、 MethodInfoのインスタンスとしてタイプからメソッド定義を取得できます。



コードビハインドの場合、型だけでなく、この型のインスタンスも必要です(結局、メソッドは静的ではなく、変換結果を発行するときにWindowインスタンスの状態を考慮する場合があります)。 幸いなことに、 ProvidValueメソッドの入力パラメーターから取得できるため、これは問題ではありません。



 public override object ProvideValue(IServiceProvider serviceProvider) { object rootObject = (serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider).RootObject; // ... }
      
      





rootObjectこれは、コードビハインドが書き込まれるオブジェクトになります。ウィンドウの場合は、 Windowオブジェクトになります。 GetTypeからGetTypeを呼び出すと、その名前が先に定義したFunctionNameプロパティで指定されているため、リフレクションを介して目的の変換メソッドを取得できます。 次に、 GenericConverterのインスタンスを作成し、受け取ったMethodInfoをインスタンスに渡し、 ProvideValueの結果としてこのコンバーターを返すだけです。



これが理論全体です。記事の最後に、この全体を実装するためのコードを示します。 メソッド名の行での私の実装は、変換メソッドとオプションの逆変換メソッドの両方を受け入れます、構文は次のようなものです:



  : '[___] [__.]__, [__.]___'      -: 'Converters.ConvertLib IntToString, StringToInt' = 'Converters.ConvertLib.IntToString, Converters.ConvertLib.StringToInt'   code-behind: 'IntToString'  one-way binding, 'IntToString, StringToInt'  two-way binding   (   code-behind,    ): 'IntToString, Converters.ConvertLib.StringToInt'
      
      





これはすべてマルチバインダーでも機能しますが、違いは変換用の関数の署名のみです(バインダーに含まれるものに対応する必要があります)。 また、 ConverterParameterは、変換関数の署名に存在する場合もあれば、存在しない場合もあります。このため、単に指定するか指定しないだけで、署名の最後のパラメーターとして定義されます。



私の実装の場合の記事で検討した例は、XAMLでは次のようになります。



 <TextBlock Text="{Binding IntProp, Converter={conv:ConvertFunc 'ConvertIntToString'}, ConverterParameter=plusOne}" /> <TextBlock> <TextBlock.Text> <MultiBinding Converter="{conv:ConvertFunc 'ConvertIntAndStringToString'}" ConverterParameter="plusOne"> <Binding Path="IntProp" /> <Binding Path="StringProp" /> </MultiBinding> </TextBlock.Text> </TextBlock>
      
      





私が見つけた私の実装の短所:



  1. メソッドの呼び出し時に、すべての種類のチェックが行われ、パラメーターの配列が作成され、一般にMethodInfo.Invoke()がメソッドを直接呼び出すほど速く動作するかどうかはわかりませんが、これがWPF / MVVMでの作業の大きなマイナスとは言えません。



  2. オーバーロードを使用する方法はありません。MethodInfoを受け取った時点では、到着する値のタイプが不明であるため、この時点では必要なメソッドオーバーロードを取得できません(どういうわけか可能ですが、方法はわかりません)。 メソッドを直接呼び出してオーバーロードを見つけるたびにリフレクションに入るオプションがまだありますが、これはすでにパーセントの不当な無駄です。 いくつかの過負荷の時間。



  3. マルチバインダーでは、渡されたパラメーターの数に応じて異なるコンバーターの動作を実行できません。 つまり、変換関数が3つのパラメーターに対して定義されている場合、マルチバインダーの数は正確にそれである必要があります。標準コンバーターでは、可変量を作成できます。


完全なソース
 using System; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Windows.Data; using System.Windows.Markup; using System.Xaml; namespace Converters { public class ConvertFuncExtension : MarkupExtension { public ConvertFuncExtension() { } public ConvertFuncExtension(string functionsExpression) { FunctionsExpression = functionsExpression; } public string FunctionsExpression { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { object rootObject = (serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider).RootObject; MethodInfo convertMethod = null; MethodInfo convertBackMethod = null; ParseFunctionsExpression(out var convertType, out var convertMethodName, out var convertBackType, out var convertBackMethodName); if (convertMethodName != null) { var type = convertType ?? rootObject.GetType(); var flags = convertType != null ? BindingFlags.Public | BindingFlags.Static : BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; if ((convertMethod = type.GetMethod(convertMethodName, flags)) == null) throw new ArgumentException($"Specified convert method {convertMethodName} not found on type {type.FullName}"); } if (convertBackMethodName != null) { var type = convertBackType ?? rootObject.GetType(); var flags = convertBackType != null ? BindingFlags.Public | BindingFlags.Static : BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; if ((convertBackMethod = type.GetMethod(convertBackMethodName, flags)) == null) throw new ArgumentException($"Specified convert method {convertBackMethodName} not found on type {type.FullName}"); } return new Converter(rootObject, convertMethod, convertBackMethod); } void ParseFunctionsExpression(out Type convertType, out string convertMethodName, out Type convertBackType, out string convertBackMethodName) { if (!ParseFunctionsExpressionWithRegex(out string commonConvertTypeName, out string fullConvertMethodName, out string fullConvertBackMethodName)) throw new ArgumentException("Error parsing functions expression"); Lazy<Type[]> allTypes = new Lazy<Type[]>(GetAllTypes); Type commonConvertType = null; if (commonConvertTypeName != null) { commonConvertType = FindType(allTypes.Value, commonConvertTypeName); if (commonConvertType == null) throw new ArgumentException($"Error parsing functions expression: type {commonConvertTypeName} not found"); } convertType = commonConvertType; convertBackType = commonConvertType; if (fullConvertMethodName != null) ParseFullMethodName(allTypes, fullConvertMethodName, ref convertType, out convertMethodName); else { convertMethodName = null; convertBackMethodName = null; } if (fullConvertBackMethodName != null) ParseFullMethodName(allTypes, fullConvertBackMethodName, ref convertBackType, out convertBackMethodName); else convertBackMethodName = null; } bool ParseFunctionsExpressionWithRegex(out string commonConvertTypeName, out string fullConvertMethodName, out string fullConvertBackMethodName) { if (FunctionsExpression == null) { commonConvertTypeName = null; fullConvertMethodName = null; fullConvertBackMethodName = null; return true; } var match = _functionsExpressionRegex.Match(FunctionsExpression.Trim()); if (!match.Success) { commonConvertTypeName = null; fullConvertMethodName = null; fullConvertBackMethodName = null; return false; } commonConvertTypeName = match.Groups[1].Value; if (commonConvertTypeName == "") commonConvertTypeName = null; fullConvertMethodName = match.Groups[2].Value.Trim(); if (fullConvertMethodName == "") fullConvertMethodName = null; fullConvertBackMethodName = match.Groups[3].Value.Trim(); if (fullConvertBackMethodName == "") fullConvertBackMethodName = null; return true; } static void ParseFullMethodName(Lazy<Type[]> allTypes, string fullMethodName, ref Type type, out string methodName) { var delimiterPos = fullMethodName.LastIndexOf('.'); if (delimiterPos == -1) { methodName = fullMethodName; return; } methodName = fullMethodName.Substring(delimiterPos + 1, fullMethodName.Length - (delimiterPos + 1)); var typeName = fullMethodName.Substring(0, delimiterPos); var foundType = FindType(allTypes.Value, typeName); type = foundType ?? throw new ArgumentException($"Error parsing functions expression: type {typeName} not found"); } static Type FindType(Type[] types, string fullName) => types.FirstOrDefault(t => t.FullName.Equals(fullName)); static Type[] GetAllTypes() => AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).ToArray(); readonly Regex _functionsExpressionRegex = new Regex( @"^(?:([^ ,]+) )?([^,]+)(?:,([^,]+))?(?:[\s\S]*)$", RegexOptions.Compiled | RegexOptions.CultureInvariant); class Converter : IValueConverter, IMultiValueConverter { public Converter(object rootObject, MethodInfo convertMethod, MethodInfo convertBackMethod) { _rootObject = rootObject; _convertMethod = convertMethod; _convertBackMethod = convertBackMethod; _convertMethodParametersCount = _convertMethod != null ? _convertMethod.GetParameters().Length : 0; _convertBackMethodParametersCount = _convertBackMethod != null ? _convertBackMethod.GetParameters().Length : 0; } #region IValueConverter object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (_convertMethod == null) return value; if (_convertMethodParametersCount == 1) return _convertMethod.Invoke(_rootObject, new[] { value }); else if (_convertMethodParametersCount == 2) return _convertMethod.Invoke(_rootObject, new[] { value, parameter }); else throw new InvalidOperationException("Method has invalid parameters"); } object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (_convertBackMethod == null) return value; if (_convertBackMethodParametersCount == 1) return _convertBackMethod.Invoke(_rootObject, new[] { value }); else if (_convertBackMethodParametersCount == 2) return _convertBackMethod.Invoke(_rootObject, new[] { value, parameter }); else throw new InvalidOperationException("Method has invalid parameters"); } #endregion #region IMultiValueConverter object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (_convertMethod == null) throw new ArgumentException("Convert function is not defined"); if (_convertMethodParametersCount == values.Length) return _convertMethod.Invoke(_rootObject, values); else if (_convertMethodParametersCount == values.Length + 1) return _convertMethod.Invoke(_rootObject, ConcatParameters(values, parameter)); else throw new InvalidOperationException("Method has invalid parameters"); } object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { if (_convertBackMethod == null) throw new ArgumentException("ConvertBack function is not defined"); object converted; if (_convertBackMethodParametersCount == 1) converted = _convertBackMethod.Invoke(_rootObject, new[] { value }); else if (_convertBackMethodParametersCount == 2) converted = _convertBackMethod.Invoke(_rootObject, new[] { value, parameter }); else throw new InvalidOperationException("Method has invalid parameters"); if (converted is object[] convertedAsArray) return convertedAsArray; // ToDo: Convert to object[] from Tuple<> and System.ValueTuple return null; } static object[] ConcatParameters(object[] parameters, object converterParameter) { object[] result = new object[parameters.Length + 1]; parameters.CopyTo(result, 0); result[parameters.Length] = converterParameter; return result; } #endregion object _rootObject; MethodInfo _convertMethod; MethodInfo _convertBackMethod; int _convertMethodParametersCount; int _convertBackMethodParametersCount; } } }
      
      





ご清聴ありがとうございました!



All Articles