Estoy trabajando en una aplicación WPF con vistas que requieren numerosas conversiones de valor. Inicialmente, mi filosofía (inspirada en parte por este animado debate sobre los Discípulos XAML ) era que debía hacer el modelo de vista estrictamente sobre el soporte de los requisitos de datos de la vista. Esto significaba que cualquier conversión de valor requerida para convertir datos en cosas como visibilidades, pinceles, tamaños, etc. se manejaría con convertidores de valor y convertidores de valores múltiples. Conceptualmente, esto parecía bastante elegante. El modelo de vista y la vista tendrían un propósito distinto y estarían muy bien desacopladas. Se trazaría una línea clara entre "datos" y "aspecto".
Bueno, después de dar a esta estrategia "el viejo intento de la universidad", tengo algunas dudas sobre si quiero seguir desarrollándome de esta manera. De hecho, estoy considerando descartar los convertidores de valor y poner la responsabilidad de (casi) toda la conversión de valor directamente en manos del modelo de vista.
La realidad del uso de convertidores de valor simplemente no parece estar a la altura del valor aparente de las preocupaciones separadas por separado. Mi mayor problema con los convertidores de valor es que son tediosos de usar. Debe crear una nueva clase, implementar IValueConverter
o IMultiValueConverter
convertir el valor o valores object
al tipo correcto, probar DependencyProperty.Unset
(al menos para convertidores de valores múltiples), escribir la lógica de conversión, registrar el convertidor en un diccionario de recursos [consulte la actualización a continuación ], y finalmente, conecte el convertidor usando XAML bastante detallado (que requiere el uso de cadenas mágicas tanto para el enlace como para el nombre del convertidor[ver actualización a continuación]). El proceso de depuración tampoco es fácil, ya que los mensajes de error a menudo son crípticos, especialmente en el modo de diseño de Visual Studio / Expression Blend.
Esto no quiere decir que la alternativa, hacer que el modelo de vista sea responsable de toda la conversión de valor, sea una mejora. Esto podría muy bien ser una cuestión de que la hierba sea más verde en el otro lado. Además de perder la elegante separación de las preocupaciones, debe escribir un montón de propiedades derivadas y asegurarse de llamar concienzudamente RaisePropertyChanged(() => DerivedProperty)
al establecer las propiedades base, lo que podría ser un problema de mantenimiento desagradable.
La siguiente es una lista inicial que reuní de los pros y los contras de permitir que los modelos de vista manejen la lógica de conversión y eliminen los convertidores de valor:
- Pros:
- Menos enlaces totales ya que se eliminan los convertidores múltiples
- Menos cadenas mágicas (rutas de enlace
+ nombres de recursos del convertidor) No más registrar cada convertidor (además de mantener esta lista)- Menos trabajo para escribir cada convertidor (no se requieren interfaces de implementación ni conversión)
- Puede inyectar dependencias fácilmente para ayudar con las conversiones (por ejemplo, tablas de colores)
- El marcado XAML es menos detallado y más fácil de leer.
- La reutilización del convertidor aún es posible (aunque se requiere algo de planificación)
- No hay problemas misteriosos con DependencyProperty.Unset (un problema que noté con los convertidores de valores múltiples)
* Los tachados indican beneficios que desaparecen si usa extensiones de marcado (consulte la actualización a continuación)
- Contras:
- Un acoplamiento más fuerte entre el modelo de vista y la vista (por ejemplo, las propiedades deben tratar conceptos como visibilidad y pinceles)
- Más propiedades totales para permitir la asignación directa para cada enlace a la vista
(consulte la Actualización 2 a continuación)RaisePropertyChanged
debe llamarse para cada propiedad derivada- Todavía debe confiar en los convertidores si la conversión se basa en una propiedad de un elemento de la interfaz de usuario
Entonces, como probablemente pueda notar, tengo un poco de acidez estomacal sobre este problema. Dudo mucho en seguir el camino de la refactorización solo para darme cuenta de que el proceso de codificación es tan ineficiente y tedioso si uso convertidores de valor o expongo numerosas propiedades de conversión de valor en mi modelo de vista.
¿Me estoy perdiendo algún pros / contras? Para aquellos que han intentado ambos medios de conversión de valor, ¿cuál encontró que funcionó mejor para usted y por qué? ¿Hay otras alternativas? (Los discípulos mencionaron algo sobre los proveedores de descriptores de tipo, pero no pude entender de qué estaban hablando. Cualquier idea sobre esto sería apreciada).
Actualizar
Hoy descubrí que es posible usar algo llamado "extensión de marcado" para eliminar la necesidad de registrar convertidores de valor. De hecho, no solo elimina la necesidad de registrarlos, sino que también proporciona inteligencia para seleccionar un convertidor cuando escribe Converter=
. Aquí está el artículo que me ayudó a comenzar: http://www.wpftutorial.net/ValueConverters.html .
La capacidad de usar una extensión de marcado cambia el equilibrio de alguna manera en mi lista de pros y contras y en la discusión anterior (ver tachado).
Como resultado de esta revelación, estoy experimentando con un sistema híbrido donde utilizo convertidores BoolToVisibility
y lo que llamo MatchToVisibility
y el modelo de vista para todas las demás conversiones. MatchToVisibility es básicamente un convertidor que me permite verificar si el valor enlazado (generalmente una enumeración) coincide con uno o más valores especificados en XAML.
Ejemplo:
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
Básicamente, lo que hace es verificar si el estado es Finalizado o Cancelado. Si es así, la visibilidad se establece en "Visible". De lo contrario, se establece en "Oculto". Esto resultó ser un escenario muy común, y tener este convertidor me ahorró alrededor de 15 propiedades en mi modelo de vista (más las declaraciones RaisePropertyChanged asociadas). Tenga en cuenta que cuando escribe Converter={vc:
, "MatchToVisibility" aparece en un menú inteligente. Esto reduce notablemente la posibilidad de errores y hace que el uso de convertidores de valor sea menos tedioso (no tiene que recordar ni buscar el nombre del convertidor de valor que desea).
En caso de que tenga curiosidad, pegaré el código a continuación. Una característica importante de esta implementación de MatchToVisibility
es que se comprueba si el valor límite es una enum
, y si lo es, se comprueba para asegurarse Value1
, Value2
, etc., también son enumeraciones del mismo tipo. Esto proporciona una verificación en tiempo de diseño y tiempo de ejecución de si alguno de los valores de enumeración está mal escrito. Para mejorar esto a una verificación en tiempo de compilación, puede usar lo siguiente (escribí esto a mano, así que perdóneme si cometí algún error):
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue={x:Type {win:Visibility.Visible}},
IfFalse={x:Type {win:Visibility.Hidden}},
Value1={x:Type {enum:Status.Finished}},
Value2={x:Type {enum:Status.Canceled}}"
Si bien esto es más seguro, es demasiado detallado para que valga la pena para mí. También podría usar una propiedad en el modelo de vista si voy a hacer esto. De todos modos, descubro que la verificación en tiempo de diseño es perfectamente adecuada para los escenarios que he probado hasta ahora.
Aquí está el código para MatchToVisibility
[ValueConversion(typeof(object), typeof(Visibility))]
public class MatchToVisibility : BaseValueConverter
{
[ConstructorArgument("ifTrue")]
public object IfTrue { get; set; }
[ConstructorArgument("ifFalse")]
public object IfFalse { get; set; }
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
[ConstructorArgument("value3")]
public object Value3 { get; set; }
[ConstructorArgument("value4")]
public object Value4 { get; set; }
[ConstructorArgument("value5")]
public object Value5 { get; set; }
public MatchToVisibility() { }
public MatchToVisibility(
object ifTrue, object ifFalse,
object value1, object value2 = null, object value3 = null,
object value4 = null, object value5 = null)
{
IfTrue = ifTrue;
IfFalse = ifFalse;
Value1 = value1;
Value2 = value2;
Value3 = value3;
Value4 = value4;
Value5 = value5;
}
public override object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var ifTrue = IfTrue.ToString().ToEnum<Visibility>();
var ifFalse = IfFalse.ToString().ToEnum<Visibility>();
var values = new[] { Value1, Value2, Value3, Value4, Value5 };
var valueStrings = values.Cast<string>();
bool isMatch;
if (Enum.IsDefined(value.GetType(), value))
{
var valueEnums = valueStrings.Select(vs => vs == null ? null : Enum.Parse(value.GetType(), vs));
isMatch = valueEnums.ToList().Contains(value);
}
else
isMatch = valueStrings.Contains(value.ToString());
return isMatch ? ifTrue : ifFalse;
}
}
Aquí está el código para BaseValueConverter
// this is how the markup extension capability gets wired up
public abstract class BaseValueConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public abstract object Convert(
object value, Type targetType, object parameter, CultureInfo culture);
public virtual object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Aquí está el método de extensión ToEnum
public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
Actualización 2
Desde que publiqué esta pregunta, me encontré con un proyecto de código abierto que usa "tejido de IL" para inyectar código NotifyPropertyChanged para propiedades y propiedades dependientes. Esto hace que implementar la visión de Josh Smith del modelo de vista como un "convertidor de valor con esteroides" sea una brisa absoluta. Simplemente puede usar "Propiedades de implementación automática", y el tejedor hará el resto.
Ejemplo:
Si ingreso este código:
public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
... esto es lo que se compila:
string givenNames;
public string GivenNames
{
get { return givenName; }
set
{
if (value != givenName)
{
givenNames = value;
OnPropertyChanged("GivenName");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
Eso es un gran ahorro en la cantidad de código que tiene que escribir, leer, desplazarse, etc. Más importante, sin embargo, le ahorra tener que averiguar cuáles son sus dependencias. Puede agregar nuevas "propiedades obtiene" comoFullName
sin tener que subir minuciosamente la cadena de dependencias para agregar RaisePropertyChanged()
llamadas.
¿Cómo se llama este proyecto de código abierto? La versión original se llama "NotifyPropertyWeaver", pero el propietario (Simon Potter) ha creado una plataforma llamada "Fody" para alojar una serie completa de tejedores IL. El equivalente de NotifyPropertyWeaver bajo esta nueva plataforma se llama PropertyChanged.Fody.
- Instrucciones de configuración de Fody: http://code.google.com/p/fody/wiki/SampleUsage (reemplace "Virtuosity" con "PropertyChanged")
- Sitio del proyecto PropertyChanged.Fody: http://code.google.com/p/propertychanged/
Si prefiere usar NotifyPropertyWeaver (que es un poco más simple de instalar, pero no necesariamente se actualizará en el futuro más allá de las correcciones de errores), aquí está el sitio del proyecto: http://code.google.com/p/ notifiquepropiedadweaver /
De cualquier manera, estas soluciones de tejido IL cambian completamente el cálculo en el debate entre el modelo de vista sobre esteroides y los convertidores de valor.
fuente
BooleanToVisibility
toma un valor relacionado con la visibilidad (verdadero / falso) y lo traduce a otro. Esto parece un uso ideal de aValueConverter
. Por otro lado,MatchToVisibility
está codificando la lógica de negocios enView
(qué tipos de elementos deberían estar visibles). En mi opinión, esta lógica debería ser empujada hacia abajoViewModel
, o incluso más allá de lo que yo llamoEditModel
. Lo que el usuario puede ver debería ser algo bajo prueba.MatchToVisibility
parecía ser una forma conveniente de habilitar algunos interruptores de modo simples (tengo una vista en particular con un montón de partes que se pueden encender y apagar. En la mayoría de los casos, las secciones de la vista incluso están etiquetadas (conx:Name
) para que coincidan con el modo corresponden a). Realmente no se me ocurrió que esto es "lógica de negocios", pero pensaré en su comentario.Respuestas:
Lo he usado
ValueConverters
en algunos casos y puse la lógicaViewModel
en otros. Mi sensación es que a seValueConverter
convierte en parte de laView
capa, por lo que si la lógica es realmente parte de la capa,View
póngala allí, de lo contrario póngala en elViewModel
.Personalmente, no veo ningún problema al
ViewModel
tratar conView
conceptos específicos comoBrush
es porque en mis aplicacionesViewModel
solo existe una superficie comprobable y enlazable para elView
. Sin embargo, algunas personas ponen mucha lógica de negocios en elViewModel
(yo no) y en ese caso elViewModel
es más como una parte de su capa de negocios, por lo que en ese caso no querría cosas específicas de WPF allí.Prefiero una separación diferente:
View
- Cosas de WPF, a veces no comprobables (como XAML y código subyacente) pero tambiénValueConverter
sViewModel
- clase comprobable y enlazable que también es específica de WPFEditModel
- parte de la capa empresarial que representa mi modelo durante la manipulaciónEntityModel
- parte de la capa empresarial que representa mi modelo como persistenteRepository
- responsable de la persistencia de laEntityModel
a la base de datosEntonces, la forma en que lo hago, tengo poco uso para
ValueConverter
sLa forma en que me alejé de algunos de tus "Con" es hacer que mis
ViewModel
"genéricos" sean muy genéricos. Por ejemplo, unoViewModel
que tengo, llamadoChangeValueViewModel
implementa una propiedad Label y una propiedad Value. En elView
hay unLabel
enlace a la propiedad Label y unTextBox
enlace a la propiedad Value.Entonces tengo uno
ChangeValueView
que estáDataTemplate
desconectado delChangeValueViewModel
tipo. Cada vez que WPF ve queViewModel
aplica esoView
. El constructor de myChangeValueViewModel
toma la lógica de interacción que necesita para actualizar su estado desdeEditModel
(generalmente solo pasando unaFunc<string>
) y la acción que debe tomar cuando el usuario edita el Valor (solo unaAction
que ejecuta alguna lógica en elEditModel
).El padre
ViewModel
(para la pantalla) toma unEditModel
en su constructor y solo crea una instancia de los elementos elementales apropiadosViewModel
comoChangeValueViewModel
. Dado que el padreViewModel
está inyectando la acción a realizar cuando el usuario realiza algún cambio, puede interceptar todas estas acciones y tomar otras acciones. Por lo tanto, la acción de edición inyectada para unChangeValueViewModel
podría verse así:Obviamente, el
foreach
bucle se puede refactorizar en otro lugar, pero lo que esto hace es tomar la acción, aplicarlo al modelo y luego (suponiendo que el modelo haya actualizado su estado de alguna manera desconocida), les dice a todos los niñosViewModel
que vayan y obtengan su estado. El modelo de nuevo. Si el estado ha cambiado, son responsables de ejecutar suPropertyChanged
eventos, según sea necesario.Eso maneja la interacción entre, digamos, un cuadro de lista y un panel de detalles bastante bien. Cuando el usuario selecciona una nueva opción, la actualiza
EditModel
con la opción yEditModel
cambia los valores de las propiedades expuestas para el panel de detalles. LosViewModel
niños que son responsables de mostrar la información del panel de detalles reciben automáticamente una notificación de que necesitan verificar nuevos valores y, si han cambiado, activan susPropertyChanged
eventos.fuente
ViewModel
capa. No todos están de acuerdo conmigo, pero depende de cómo funcione su arquitectura.CalendarViewModel
para unCalendarView
UserControl o unDialogViewModel
para aDialogView
). Sin embargo, esa es solo mi opinión :)ViewModel
s.Si la conversión es algo relacionado con la vista, como decidir la visibilidad de un objeto, determinar qué imagen mostrar o averiguar qué color de pincel usar, siempre pongo mis convertidores en la vista.
Si está relacionado con el negocio, como determinar si un campo debe enmascararse, o si un usuario tiene permiso para realizar una acción, la conversión se realiza en mi ViewModel.
Desde sus ejemplos, creo que se está perdiendo una gran pieza de WPF:
DataTriggers
. Parece que está usando convertidores para determinar valores condicionales, pero los convertidores realmente deberían ser para convertir un tipo de datos en otro.En tu ejemplo anterior
Usaría a
DataTrigger
para determinar qué imagen mostrar, no aConverter
. Un convertidor es para convertir un tipo de datos a otro, mientras que un activador se utiliza para determinar algunas propiedades en función de un valor.La única vez que consideraría usar un convertidor para esto es si el valor enlazado realmente contiene los datos de la imagen, y necesito convertirlo en un tipo de datos que la IU pueda entender. Por ejemplo, si la fuente de datos contiene una propiedad llamada
ImageFilePath
, entonces consideraría usar un convertidor para convertir la cadena que contiene la ubicación del archivo de imagen en unaBitmapImage
que podría usarse como la fuente de mi imagenEl resultado final es que tengo un espacio de nombres de biblioteca lleno de convertidores genéricos que convierten un tipo de datos a otro, y rara vez tengo que codificar un nuevo convertidor. Hay ocasiones en las que querré convertidores para conversiones específicas, pero son tan poco frecuentes que no me importa escribirlas.
fuente
Grid
elementos completos . También estoy tratando de hacer cosas como establecer pinceles para primer plano / fondo / trazo en función de los datos en mi modelo de vista y una paleta de colores específica definida en el archivo de configuración. No estoy seguro de que este sea un gran ajuste para un disparador o un convertidor. El único problema que tengo hasta ahora al poner la mayoría de la lógica de vista en el modelo de vista es conectar todas lasRaisePropertyChanged()
llamadas.DataTrigger
, incluso cambiando los elementos de Grid. Por lo general, coloco un lugarContentControl
donde debería estar mi contenido dinámico y lo cambioContentTemplate
en un disparador. Tengo un ejemplo en el siguiente enlace si está interesado (desplácese hacia abajo a la sección con el encabezado deUsing a DataTrigger
) rachel53461.wordpress.com/2011/05/28/…<TextBlock Text="I'm a Person" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Person}}"
y<TextBlock Text="I'm a Business" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Business}}"
Depende de lo que esté probando , en todo caso.
Sin pruebas: mezcle el código de vista con ViewModel a voluntad (siempre puede refactorizar más adelante).
Pruebas en ViewModel y / o versiones inferiores: use convertidores.
Pruebas en capas de modelo y / o inferiores: entremezcle el código de vista con ViewModel a voluntad
ViewModel abstrae el Modelo para la Vista . Personalmente, usaría ViewModel para Pinceles, etc. y me saltaría los convertidores. Pruebe en la (s) capa (s) donde los datos están en su forma " más pura " (es decir, capas de modelo ).
fuente
Visibility
,SolidColorBrush
yThickness
.Esto probablemente no resolverá todos los problemas que mencionó, pero hay dos puntos a considerar:
Primero, debe colocar el código del convertidor en algún lugar de su primera estrategia. ¿Considera esa parte de la vista o el modelo de vista? Si es parte de la vista, ¿por qué no colocar las propiedades específicas de la vista en la vista en lugar del modelo de vista?
En segundo lugar, parece que su diseño sin convertidor intenta modificar las propiedades reales de los objetos que ya existen. Parece que ya implementan INotifyPropertyChanged, entonces, ¿por qué no usar crear un objeto contenedor específico de vista para enlazar? Aquí hay un ejemplo simple:
fuente
A veces es bueno usar un convertidor de valor para aprovechar la virtualización.
Un ejemplo de esto es qué en un proyecto en el que tuvimos que mostrar datos enmascarados para cientos de miles de celdas en una cuadrícula. Cuando decodificamos las máscaras de bits en el modelo de vista para cada celda, el programa tardó demasiado en cargarse.
Pero cuando creamos un convertidor de valor que decodificó una sola celda, el programa se cargó en una fracción del tiempo y fue igual de receptivo porque el convertidor solo se llama cuando el usuario está mirando una celda en particular (y solo necesitaría ser llamado un máximo de treinta veces cada vez que el usuario cambia su vista en la cuadrícula).
No sé cómo MVVM se quejó de esa solución, pero redujo el tiempo de carga en un 95%.
fuente