📜 ⬆️ ⬇️

Simplify converters for WPF

About a year already working with WPF and some things in it frankly vybeshivayut. One of these things is converters. For the sake of every sneeze, declare the implementation of a dubious looking interface somewhere in the depths of the project, and then search for it via Ctrl + F by name when it is suddenly needed. In multi-converters, so the devil himself will get confused.

The situation is aggravated by MVVM, due to which not using this miracle of science is quite rare. Well, it's time to ease the routine of creating and using converters a bit, let's go.

Immediately, I’ll make a reservation that I don’t mind the often used converters like BooleanToVisibilityConverter and the like, they can be easily remembered and reused in many places. But it often happens that the converter needs some very specific, and somehow you don’t want to make a whole component out of it. And for a long time, and clogs the global scope, then it is difficult to find the right in all this garbage.

Converters are used when working with binding-s and allow you to convert values ​​in one-sided or two-sided order (depending on the binding mode). Converters are also of two types - with one value and with many. Interfaces IValueConverter and IMultiValueConverter respectively are responsible for them.
')
With a single value, we use regular binding, usually through the BindingBase markup extension built into XAML:

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

In the case of a multi-value, this monstrous design is used:
 <TextBlock> <TextBlock.Text> <MultiBinding Converter="{StaticResource conv:IntToStringConverter}" ConverterParameter="plusOne"> <Binding Path="IntProp" /> <Binding Path="StringProp" /> </MultiBinding> </TextBlock.Text> </TextBlock> 

The converters themselves will look like this (There are two converters in one class at once, but it can be done separately):

 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(); } 

This is extremely short, since the example is synthetic, but already here without a half liter it’s not a damn thing to know what the arrays are, what the type cast, some left targetType and culture, what ConvertBack is without the implementation.

I have several ideas to simplify this:

  1. Converters in the form of pieces of c # code directly into xaml for simple calculations;
  2. Converters in the form of references to methods in the code-behind, for cases with very specific / special cases of conversion, that is, when there is no point in converting this to anywhere else;
  3. Converters in the form of the same that in the standard implementation, but so that it does not look so dumb that every time you write a new converter you do not have to go to Google and look for an example of the implementation of the converter.

Immediately break off those of you who think that I will tell you how to implement paragraph 1. I will not. The network has several implementations of this, for example here . I also saw variants with expression tree and it seems some more. Such things are suitable only for the simplest cases - to work with arithmetic and logical operations. If there need to call some classes, use strings, and so on, then problems with escaping inside the xml and the problem of including namespaces will come out. However, in the simplest cases, such things are quite possible to use.

But we will consider 2 and 3 points in more detail. Suppose it was necessary to determine the method of converting to the code-behind. What should it look like? I think something like this:

 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}"; 

Compare this with the previous option. Code less - more clarity. Similarly, a variant with a separate reused converter might look like:

 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}"; } 

Not bad, huh? Well, how do you make xaml friends with this, because he understands only standard converter interfaces? Of course, for each such class, you can make a wrapper in the form of standard IValueConverter / IMultiValueConverter which will already use beautiful methods, but then the whole point is lost if you have to declare a wrapper for each readable converter. One solution is to make such a wrapper universal, like this:

 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) { //    ,    } } 

This is all in theory, how can delegates be practically transferred to the converter, and how to get them with only XAML?

Markup extension, MarkupExtension, comes to the rescue. It is enough to inherit the MarkupExtension class and override the ProvideValue method and in XAML it will be possible to write Binding-like expressions in curly brackets, but with their own working mechanisms.

In order to pass a link to the conversion methods through markup extensions, the simplest thing is to use their string names. Let's agree that the code-behind methods will be defined simply by the method name, and the static methods in external libraries will go like ClrNamespace.ClassName.MethodName , they can be distinguished by the presence of a dot in the latter (At least one dot will be between the class name and the method, if the class lies in the global namespace).

How to identify the methods figured out, how to get them in the markup extension in the form of delegates to pass to the converter? The markup extension ( MarkupExtension ) has a ProvideValue method for overriding, which looks like this:

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

The override method must return what is eventually assigned to the property, in the value of which this XAML markup defines this markup extension. This method can return any value, but since we will substitute this markup extension into the Converter property for binding (or multi-binding), the return value should be a converter, that is, an instance of the IValueConverter / IMultiValueConverter type . Again, it makes no sense to make different converters, you can make one class and implement these two interfaces at once, so that the converter fits both for a single binding and for a multiple one.

In order to pass a string to the markup extension, which defines the name of the function from the code-behind or static library that the converter should invoke, you need to define a public string property in the MarkupExtension instance:

 public string FunctionName { get; set; } 

After that, it will be possible to write in markup like this:

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

However, this can be simplified, for starters, it is not necessary to write Extension in the name of the extension class conv: GenericConvExtension in XAML, simply conv: GenericConv. Further in the extension you can define a constructor in order not to explicitly specify the name of the property with the name of the function:

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

Now the XAML expression is even simpler:

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

Pay attention to the absence of quotes in the name of the conversion function. In cases where there are no spaces or other unhealthy characters in the string, single quotes are optional.

Now it only remains to get a reference to the method in the ProvideValue method, create an instance of the converter and pass this link to it. A reference to a method can be obtained through the Reflection mechanism, but for this you need to know the runtime type in which this method is declared. In the case of the implementation of conversion methods in static classes, the full name of the static method is passed (With the full class name indicated), respectively, you can parse this string, type the full name of the type through Reflection to get the type, and also get the method definition as an instance of MethodInfo from the type.

In the case of the code-behind, you need not only the type, but also an instance of this type (After all, the method may not be static and take into account the Window state of the instance when issuing the conversion result). Fortunately, this is not a problem, since it can be obtained through the input parameter of the ProvideValue method:

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

rootObject is the object in which the code-behind is written; in the case of a window, it will be a Window object. By calling GetType in it, you can get a conversion method that interests us through reflection, since its name is specified in the FunctionName property defined earlier. Then you just need to create an instance of GenericConverter , passing in it the received MethodInfo and return this converter as a result of ProvideValue .

That's the whole theory, at the end of the article I will give the code for my implementation of this whole business. My implementation in the line with the name of the method accepts both the conversion method and optionally the reverse conversion method, the syntax is something like this:

  : '[___] [__.]__, [__.]___'      -: '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' 

It also works with multi-binders, the difference will be only in the signature of the functions for conversion (it should correspond to what goes in the binding). Also, the ConverterParameter may be present in the signature of the conversion function, or it may be absent, for this it simply needs to be specified, or not specified, it is defined as simply the last parameter in the signature.

The example considered in the article in the case of my implementation will look like this in 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> 

Cons of my implementation, which I found:

  1. At the time of the method call, all sorts of checks are going on, creating arrays for the parameters, and in general I'm not sure that MethodInfo.Invoke () works as quickly as calling the method directly, but I would not say that this is a big minus in WPF / MVVM .

  2. It is not possible to use overloads, because at the time of receiving MethodInfo, the types of values ​​that will be received are unknown, which means you cannot get the desired overload of the method at this moment (Maybe you can somehow, but I don’t know how). There is another option to crawl into reflection every time when you directly call the method and find an overload, but this will already be an unjustified waste of percents. time for some overload.

  3. The impossibility in multi-binning to do different behavior of the converter depending on the number of parameters passed. That is, if the conversion function is defined for 3 parameters, then the number of multi-bindings should be exactly the same; in a standard converter, you can make a variable number.

Full source
 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; } } } 

Thanks for attention!

Source: https://habr.com/ru/post/326646/


All Articles