Estou trabalhando em um aplicativo WPF com visualizações que exigem inúmeras conversões de valor. Inicialmente, minha filosofia (inspirada em parte por este animado debate sobre os discípulos XAML ) era que eu deveria fazer o modelo de exibição estritamente sobre o suporte aos requisitos de dados da exibição. Isso significava que quaisquer conversões de valor necessárias para transformar dados em coisas como visibilidades, pincéis, tamanhos etc. seriam tratadas com conversores de valor e conversores de vários valores. Conceitualmente, isso parecia bastante elegante. O modelo de visão e a visão teriam um propósito distinto e seriam bem dissociados. Uma linha clara seria traçada entre "dados" e "aparência".
Bem, depois de dar a essa estratégia "a velha faculdade tentar", estou com algumas dúvidas se quero continuar desenvolvendo dessa maneira. Na verdade, estou pensando fortemente em descartar os conversores de valor e colocar a responsabilidade por (quase) toda a conversão de valor diretamente nas mãos do modelo de exibição.
A realidade do uso de conversores de valor simplesmente não parece estar medindo o valor aparente de preocupações limpas. Meu maior problema com os conversores de valor é que eles são tediosos de usar. É necessário criar uma nova classe, implementar IValueConverter
ou IMultiValueConverter
converter os valores object
para o tipo correto, testar DependencyProperty.Unset
(pelo menos para conversores de vários valores), escrever a lógica de conversão, registrar o conversor em um dicionário de recursos [ver atualização abaixo ] e, finalmente, conecte o conversor usando XAML bastante detalhado (que requer o uso de seqüências de caracteres mágicas para a (s) ligação (ões) e o nome do conversor[veja atualização abaixo]). O processo de depuração também não é um piquenique, pois as mensagens de erro geralmente são enigmáticas, especialmente no modo de design do Visual Studio / Expression Blend.
Isso não quer dizer que a alternativa - tornar o modelo de visualização responsável por toda a conversão de valor - seja uma melhoria. Isso poderia muito bem ser uma questão de a grama ser mais verde do outro lado. Além de perder a elegante separação de preocupações, você deve escrever várias propriedades derivadas e ligar conscientemente RaisePropertyChanged(() => DerivedProperty)
ao definir propriedades básicas, o que pode ser um problema desagradável de manutenção.
A seguir, é apresentada uma lista inicial que reuni os prós e os contras de permitir que os modelos de exibição manejem a lógica de conversão e acabem com os conversores de valor:
- Prós:
- Menos ligações totais desde que os multi-conversores são eliminados
- Menos seqüências de caracteres mágicas (caminhos de ligação
+ nomes de recursos do conversor) Não é mais necessário registrar cada conversor (além de manter esta lista)- Menos trabalho para escrever cada conversor (não são necessárias interfaces de implementação ou conversão)
- Pode injetar facilmente dependências para ajudar nas conversões (por exemplo, tabelas de cores)
- A marcação XAML é menos detalhada e mais fácil de ler
- A reutilização do conversor ainda é possível (embora seja necessário algum planejamento)
- Sem problemas misteriosos com DependencyProperty.Unset (um problema que notei com conversores de vários valores)
* As rasuras indicam os benefícios que desaparecem se você usar extensões de marcação (consulte a atualização abaixo)
- Contras:
- Acoplamento mais forte entre o modelo de vista e a vista (por exemplo, as propriedades devem lidar com conceitos como visibilidade e pincéis)
- Mais propriedades totais para permitir o mapeamento direto para todas as ligações vistas
(consulte a Atualização 2 abaixo)RaisePropertyChanged
deve ser chamado para cada propriedade derivada- Ainda deve contar com conversores se a conversão for baseada na propriedade de um elemento da interface do usuário
Então, como você provavelmente pode dizer, eu tenho um pouco de azia sobre esse problema. Estou muito hesitante em seguir o caminho da refatoração apenas para perceber que o processo de codificação é tão ineficiente e tedioso se eu uso conversores de valor ou expondo inúmeras propriedades de conversão de valor no meu modelo de exibição.
Estou faltando algum prós / contras? Para aqueles que tentaram os dois meios de conversão de valor, qual você achou que funcionou melhor para você e por quê? Há alguma outra alternativa? (Os discípulos mencionaram algo sobre os provedores de descritores de tipo, mas eu não conseguia entender o que eles estavam falando. Qualquer visão sobre isso seria apreciada.)
Atualizar
Descobri hoje que é possível usar algo chamado "extensão de marcação" para eliminar a necessidade de registrar conversores de valor. De fato, ele não apenas elimina a necessidade de registrá-los, mas também fornece inteligência para selecionar um conversor quando você digita Converter=
. Aqui está o artigo que me iniciou: http://www.wpftutorial.net/ValueConverters.html .
A capacidade de usar uma extensão de marcação altera um pouco o equilíbrio na minha lista de prós e contras e na discussão acima (consulte tacadas).
Como resultado dessa revelação, estou experimentando um sistema híbrido no qual uso conversores BoolToVisibility
e o que chamo de MatchToVisibility
modelo de exibição para todas as outras conversões. MatchToVisibility é basicamente um conversor que permite verificar se o valor associado (geralmente uma enumeração) corresponde a um ou mais valores especificados em XAML.
Exemplo:
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
Basicamente, o que isso faz é verificar se o status é Concluído ou Cancelado. Se for, a visibilidade será definida como "Visível". Caso contrário, ele obtém conjuntos para "Oculto". Isso acabou sendo um cenário muito comum, e ter esse conversor me salvou cerca de 15 propriedades no meu modelo de exibição (mais as instruções RaisePropertyChanged associadas). Observe que, quando você digita Converter={vc:
, "MatchToVisibility" aparece em um menu intellisense. Isso reduz visivelmente a chance de erros e torna o uso de conversores de valor menos tedioso (você não precisa se lembrar ou procurar o nome do conversor de valor que deseja).
Caso você esteja curioso, colarei o código abaixo. Uma característica importante deste implementação MatchToVisibility
é que ele verifica para ver se o valor limite é um enum
, e se for, ele verifica para certificar-se Value1
, Value2
etc. também são enums do mesmo tipo. Isso fornece uma verificação em tempo de design e tempo de execução se algum dos valores da enumeração está digitado incorretamente. Para melhorar isso para uma verificação em tempo de compilação, você pode usar o seguinte (digitei manualmente, por favor, perdoe-me se cometi algum erro):
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}}"
Embora isso seja mais seguro, é muito detalhado para valer a pena para mim. É melhor usar apenas uma propriedade no modelo de exibição se eu fizer isso. De qualquer forma, estou descobrindo que a verificação do tempo de design é perfeitamente adequada para os cenários que tentei até agora.
Aqui está o 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;
}
}
Aqui está o 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();
}
}
Aqui está o método de extensão ToEnum
public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
Atualização 2
Desde que publiquei essa pergunta, deparei-me com um projeto de código aberto que usa "IL weaving" para injetar o código NotifyPropertyChanged para propriedades e propriedades dependentes. Isso torna a implementação da visão de Josh Smith do modelo de visualização como um "conversor de valor em esteróides" uma brisa absoluta. Você pode simplesmente usar "Propriedades Auto-Implementadas", e o tecelão fará o resto.
Exemplo:
Se eu inserir este código:
public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
... isto é o que é compilado:
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);
}
}
Isso representa uma enorme economia na quantidade de código que você precisa digitar, ler, rolar, etc. Mais importante, porém, evita que você precise descobrir quais são suas dependências. Você pode adicionar novas "propriedades obtidas", FullName
sem precisar subir meticulosamente a cadeia de dependências para adicionar RaisePropertyChanged()
chamadas.
Como é chamado esse projeto de código aberto? A versão original é chamada "NotifyPropertyWeaver", mas o proprietário (Simon Potter) criou uma plataforma chamada "Fody" para hospedar toda uma série de tecelões de IL. O equivalente a NotifyPropertyWeaver nesta nova plataforma é chamado PropertyChanged.Fody.
- Instruções de configuração do Fody: http://code.google.com/p/fody/wiki/SampleUsage (substitua "Virtuosity" por "PropertyChanged")
- Site do projeto PropertyChanged.Fody: http://code.google.com/p/propertychanged/
Se você preferir usar o NotifyPropertyWeaver (que é um pouco mais simples de instalar, mas não será necessariamente atualizado no futuro além das correções de bugs), aqui está o site do projeto: http://code.google.com/p/ notifypropertyweaver /
De qualquer maneira, essas soluções de tecelagem de IL alteram completamente o cálculo no debate entre o modelo de exibição em esteróides versus conversores de valor.
MatchToVisibility
parecia ser uma maneira conveniente de ativar alguns comutadores de modo simples (eu tenho uma visualização em particular com várias partes que podem ser ativadas e desativadas. Na maioria dos casos, as seções da visualização são até rotuladas (com x:Name
) para corresponder ao modo eles correspondem.) Realmente não me ocorreu que isso seja "lógica de negócios", mas vou pensar um pouco no seu comentário.
BooleanToVisibility
pega um valor relacionado à visibilidade (verdadeiro / falso) e o converte em outro. Este parece ser o uso ideal de aValueConverter
. Por outro lado,MatchToVisibility
está codificando a lógica de negócios noView
(que tipos de itens devem estar visíveis). Na minha opinião, essa lógica deve ser empurrada para oViewModel
, ou ainda mais, para o que eu chamo deEditModel
. O que o usuário pode ver deve ser algo em teste.