c# - ¿Cómo puede vincularse a un DynamicResource para que pueda usar un convertidor o StringFormat, etc.?(Revisión 4)
wpf ivalueconverter (1)
Hay algo que siempre sentí que faltaba un poco de funcionalidad en WPF: la capacidad de usar un recurso dinámico como fuente de un enlace.
Técnicamente entiendo por qué esto es así: para detectar cambios, el origen de un enlace debe ser una propiedad en un
DependencyObject
o en un objeto que admita
INotifyPropertyChanged
, y un recurso dinámico es en realidad una
ResourceReferenceExpression
interna de Microsoft que equivale a la
valor
del recurso (es decir, no es un objeto con una propiedad a la que vincularse, y mucho menos uno con notificación de cambio), pero aún así, siempre me molestó que, como algo que puede cambiar durante el tiempo de ejecución, debería poder ser empujado a través de un convertidor según sea necesario.
Bueno, creo que finalmente he rectificado esta limitación ...
¡Ingrese DynamicResourceBinding !
Nota: Lo llamo ''Enlace'', pero técnicamente es una
MarkupExtension
de
MarkupExtension
en la que he definido propiedades como
Converter
,
ConverterParameter
,
ConverterCulture
, etc., pero que finalmente usa un enlace internamente (¡varios, en realidad!) Como tal, yo lo hemos nombrado en función de su uso, no de su tipo real.
¿Pero por qué?
Entonces, ¿por qué necesitarías hacer esto?
¿Qué tal escalar globalmente el tamaño de su fuente en función de las preferencias del usuario y al mismo tiempo poder utilizar tamaños de fuente relativos gracias a un
MultiplyByConverter
?
¿O qué tal definir márgenes para toda la aplicación basados simplemente en un recurso
double
mediante el uso de un
DoubleToThicknessConverter
que no solo lo convierte en un grosor, sino que le permite enmascarar los bordes según sea necesario en el diseño?
¿O qué tal definir un
ThemeColor
base en un recurso, luego usar un convertidor para aclararlo u oscurecerlo, o cambiar su opacidad dependiendo del uso gracias a un
ColorShadingConverter
?
¡Aún mejor, implemente lo anterior como
MarkupExtension
sy su XAML también se simplifica!
<!-- Make the font size 85% of what it would normally be here -->
<TextBlock FontSize="{res:FontSize Scale=0.85)" />
<!-- Use the common margin, but suppress the top edge -->
<Border Margin="{res:Margin Mask=1011)" />
En resumen, esto ayuda a consolidar todos los ''valores base'' en sus recursos principales, pero podrá ajustarlos cuando y donde se usen sin tener que incluir ''x'' número de variaciones en su colección de recursos.
La salsa mágica
La implementación de
DynamicResourceBinding
se debe a un buen truco del tipo de datos
Freezable
.
Específicamente...
Si agrega un Freezable a la colección Resources de un FrameworkElement, cualquier propiedad de dependencia de ese objeto Freezable que se establezca como recurso dinámico resolverá esos recursos en relación con la posición de ese FrameworkElement en el Árbol visual.
Usando ese poco de ''salsa mágica'', el truco consiste en establecer un
DynamicResource
en una
DependencyProperty
de un objeto
Freezable
proxy, agregar ese
Freezable
a la colección de recursos del
FrameworkElement
objetivo, luego configurar un enlace entre los dos, que ahora está permitido , ya que la fuente ahora es un
DependencyObject
(es decir, un
Freezable
).
La complejidad es obtener el
FrameworkElement
destino cuando se usa esto en un
Style
, ya que
MarkupExtension
proporciona su valor donde está definido, no donde se aplica finalmente su resultado.
Esto significa que cuando usa un
MarkupExtension
directamente en un
FrameworkElement
, su objetivo es el
FrameworkElement
como era de esperar.
Sin embargo, cuando utiliza un
MarkupExtension
en un estilo, el objeto
Style
es el objetivo del
MarkupExtension
, no el
FrameworkElement
donde se aplica.
Gracias al uso de un segundo enlace interno, también he logrado sortear esta limitación.
Dicho esto, aquí está la solución con comentarios en línea:
DynamicResourceBinding
La ''Salsa Mágica'' Lea los comentarios en línea de lo que está sucediendo.
public class DynamicResourceBindingExtension : MarkupExtension {
public DynamicResourceBindingExtension(){}
public DynamicResourceBindingExtension(object resourceKey)
=> ResourceKey = resourceKey ?? throw new ArgumentNullException(nameof(resourceKey));
public object ResourceKey { get; set; }
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public CultureInfo ConverterCulture { get; set; }
public string StringFormat { get; set; }
public object TargetNullValue { get; set; }
private BindingProxy bindingSource;
private BindingTrigger bindingTrigger;
public override object ProvideValue(IServiceProvider serviceProvider) {
// Get the binding source for all targets affected by this MarkupExtension
// whether set directly on an element or object, or when applied via a style
var dynamicResource = new DynamicResourceExtension(ResourceKey);
bindingSource = new BindingProxy(dynamicResource.ProvideValue(null)); // Pass ''null'' here
// Set up the binding using the just-created source
// Note, we don''t yet set the Converter, ConverterParameter, StringFormat
// or TargetNullValue (More on that below)
var dynamicResourceBinding = new Binding() {
Source = bindingSource,
Path = new PropertyPath(BindingProxy.ValueProperty),
Mode = BindingMode.OneWay
};
// Get the TargetInfo for this markup extension
var targetInfo = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
// Check if this is a DependencyObject. If so, we can set up everything right here.
if(targetInfo.TargetObject is DependencyObject dependencyObject){
// Ok, since we''re being applied directly on a DependencyObject, we can
// go ahead and set all those missing properties on the binding now.
dynamicResourceBinding.Converter = Converter;
dynamicResourceBinding.ConverterParameter = ConverterParameter;
dynamicResourceBinding.ConverterCulture = ConverterCulture;
dynamicResourceBinding.StringFormat = StringFormat;
dynamicResourceBinding.TargetNullValue = TargetNullValue;
// If the DependencyObject is a FrameworkElement, then we also add the
// bindingSource to its Resources collection to ensure proper resource lookup
if (dependencyObject is FrameworkElement targetFrameworkElement)
targetFrameworkElement.Resources.Add(bindingSource, bindingSource);
// And now we simply return the same value as if we were a true binding ourselves
return dynamicResourceBinding.ProvideValue(serviceProvider);
}
// Ok, we''re not being set directly on a DependencyObject (most likely we''re being set via a style)
// so we need to get the ultimate target of the binding.
// We do this by setting up a wrapper MultiBinding, where we add the above binding
// as well as a second binding which we create using a RelativeResource of ''Self'' to get the target,
// and finally, since we have no way of getting the BindingExpressions (as there will be one wherever
// the style is applied), we create a third child binding which is a convenience object on which we
// trigger a change notification, thus refreshing the binding.
var findTargetBinding = new Binding(){
RelativeSource = new RelativeSource(RelativeSourceMode.Self)
};
bindingTrigger = new BindingTrigger();
var wrapperBinding = new MultiBinding(){
Bindings = {
dynamicResourceBinding,
findTargetBinding,
bindingTrigger.Binding
},
Converter = new InlineMultiConverter(WrapperConvert)
};
return wrapperBinding.ProvideValue(serviceProvider);
}
// This gets called on every change of the dynamic resource, for every object it''s been applied to
// either when applied directly, or via a style
private object WrapperConvert(object[] values, Type targetType, object parameter, CultureInfo culture) {
var dynamicResourceBindingResult = values[0]; // This is the result of the DynamicResourceBinding**
var bindingTargetObject = values[1]; // The ultimate target of the binding
// We can ignore the bogus third value (in ''values[2]'') as that''s the dummy result
// of the BindingTrigger''s value which will always be ''null''
// ** Note: This value has not yet been passed through the converter, nor been coalesced
// against TargetNullValue, or, if applicable, formatted, both of which we have to do here.
if (Converter != null)
// We pass in the TargetType we''re handed here as that''s the real target. Child bindings
// would''ve normally been handed ''object'' since their target is the MultiBinding.
dynamicResourceBindingResult = Converter.Convert(dynamicResourceBindingResult, targetType, ConverterParameter, ConverterCulture);
// Check the results for null. If so, assign it to TargetNullValue
// Otherwise, check if the target type is a string, and that there''s a StringFormat
// if so, format the string.
// Note: You can''t simply put those properties on the MultiBinding as it handles things differently
// than a single binding (i.e. StringFormat is always applied, even when null.
if (dynamicResourceBindingResult == null)
dynamicResourceBindingResult = TargetNullValue;
else if (targetType == typeof(string) && StringFormat != null)
dynamicResourceBindingResult = String.Format(StringFormat, dynamicResourceBindingResult);
// If the binding target object is a FrameworkElement, ensure the BindingSource is added
// to its Resources collection so it will be part of the lookup relative to the FrameworkElement
if (bindingTargetObject is FrameworkElement targetFrameworkElement
&& !targetFrameworkElement.Resources.Contains(bindingSource)) {
// Add the resource to the target object''s Resources collection
targetFrameworkElement.Resources[bindingSource] = bindingSource;
// Since we just added the source to the visual tree, we have to re-evaluate the value
// relative to where we are. However, since there''s no way to get a binding expression,
// to trigger the binding refresh, here''s where we use that BindingTrigger created above
// to trigger a change notification, thus having it refresh the binding with the (possibly)
// new value.
// Note: since we''re currently in the Convert method from the current operation,
// we must make the change via a ''Post'' call or else we will get results returned
// out of order and the UI won''t refresh properly.
SynchronizationContext.Current.Post((state) => {
bindingTrigger.Refresh();
}, null);
}
// Return the now-properly-resolved result of the child binding
return dynamicResourceBindingResult;
}
}
BindingProxy
Este es el
Freezable
mencionado anteriormente, pero también es útil para otros patrones vinculantes relacionados con el proxy en los que necesita cruzar los límites de los árboles visuales.
Busque aquí o en Google ''BindingProxy'' para obtener más información sobre ese otro uso.
¡Es genial!
public class BindingProxy : Freezable {
public BindingProxy(){}
public BindingProxy(object value)
=> Value = value;
protected override Freezable CreateInstanceCore()
=> new BindingProxy();
#region Value Property
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
nameof(Value),
typeof(object),
typeof(BindingProxy),
new FrameworkPropertyMetadata(default));
public object Value {
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
#endregion Value Property
}
Nota: Nuevamente, debe usar un Freezable para que esto funcione. Insertar cualquier otro tipo de DependencyObject en los recursos de FrameworkElement de destino, irónicamente, incluso otro FrameworkElement, resolverá DynamicResources en relación con la Aplicación y no el FrameworkElement asociado como no Freezables en la colección de Recursos no participa en la búsqueda de recursos localizados. Como resultado, pierde todos los recursos que se pueden definir dentro del árbol visual.
BindingTrigger
Esta clase se usa para forzar la actualización de
MultiBinding
ya que no tenemos acceso a la última
BindingExpression
.
(Técnicamente, puede usar cualquier clase que admita notificaciones de cambio, pero personalmente me gusta que mis diseños sean explícitos en cuanto a su uso).
public class BindingTrigger : INotifyPropertyChanged {
public BindingTrigger()
=> Binding = new Binding(){
Source = this,
Path = new PropertyPath(nameof(Value))};
public event PropertyChangedEventHandler PropertyChanged;
public Binding Binding { get; }
public void Refresh()
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
public object Value { get; }
}
InlineMultiConverter
Esto le permite configurar convertidores fácilmente en código subyacente simplemente proporcionando los métodos a utilizar para la conversión. (Tengo uno similar para InlineConverter)
public class InlineMultiConverter : IMultiValueConverter {
public delegate object ConvertDelegate (object[] values, Type targetType, object parameter, CultureInfo culture);
public delegate object[] ConvertBackDelegate(object value, Type[] targetTypes, object parameter, CultureInfo culture);
public InlineMultiConverter(ConvertDelegate convert, ConvertBackDelegate convertBack = null){
_convert = convert ?? throw new ArgumentNullException(nameof(convert));
_convertBack = convertBack;
}
private ConvertDelegate _convert { get; }
private ConvertBackDelegate _convertBack { get; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
=> _convert(values, targetType, parameter, culture);
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> (_convertBack != null)
? _convertBack(value, targetTypes, parameter, culture)
: throw new NotImplementedException();
}
Uso
Al igual que con un enlace regular, así es como lo usa (suponiendo que haya definido un recurso ''doble'' con la clave ''MyResourceKey'') ...
<TextBlock Text="{drb:DynamicResourceBinding ResourceKey=MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat=''Four times the resource is {0}''}" />
o incluso más corto, puede omitir ''ResourceKey ='' gracias a la sobrecarga del constructor para que coincida con cómo funciona ''Path'' en un enlace regular ...
<TextBlock Text="{drb:DynamicResourceBinding MyResourceKey, Converter={cv:MultiplyConverter Factor=4}, StringFormat=''Four times the resource is {0}''}" />
¡Ahí lo tienes!
¡
DynamicResource
a un
DynamicResource
con soporte completo para convertidores, formatos de cadena, manejo de valores nulos, etc.!
De todos modos, eso es todo! Realmente espero que esto ayude a otros desarrolladores ya que realmente ha simplificado nuestras plantillas de control, especialmente en torno a grosores de borde comunes y demás.
¡Disfrutar!
Nota: Esta es una revisión de un diseño anterior que tenía la limitación de no ser utilizable en un estilo, negando bastante su efectividad. Sin embargo, esta nueva versión ahora funciona con estilos , esencialmente permitiéndole usarla en cualquier lugar donde pueda usar un enlace o un recurso dinámico y obtener los resultados esperados, haciéndolo inmensamente más útil.
Técnicamente, esto no es una pregunta.
Es una publicación que muestra una forma en que descubrí que utilizo fácilmente convertidores con un
DynamicResource
como fuente, pero para seguir las mejores prácticas de s / o, lo publico como un par de preguntas / respuestas.
Así que mira mi respuesta a continuación sobre la forma en que encontré cómo hacer esto.
¡Espero eso ayude!