Pracuję nad aplikacją WPF z widokami, które wymagają wielu konwersji wartości. Początkowo moją filozofią (zainspirowaną częściowo tą ożywioną debatą na temat XAML Disciples ) było to, że powinienem stworzyć model widoku ściśle uwzględniający wymagania dotyczące danych w widoku. Oznaczało to, że wszelkie konwersje wartości wymagane do przekształcenia danych w rzeczy takie jak widoczności, pędzle, rozmiary itp. Będą obsługiwane przez konwertery wartości i konwertery wielu wartości. Pod względem koncepcyjnym wydawało się to dość eleganckie. Model widoku i widok miałyby odrębny cel i byłyby ładnie oddzielone. Należałoby wytyczyć wyraźną linię między „danymi” a „wyglądem”.
Cóż, po wypróbowaniu tej strategii jako „starej próby na studiach”, mam wątpliwości, czy chcę nadal rozwijać się w ten sposób. Naprawdę mocno zastanawiam się nad odrzuceniem konwerterów wartości i przekazaniem odpowiedzialności za (prawie) całą konwersję wartości wprost w ręce modelu widoku.
Rzeczywistość używania konwerterów wartości po prostu nie wydaje się mierzyć do wartości pozornie wyodrębnionych problemów. Moim największym problemem z konwerterami wartości jest to, że są uciążliwe w użyciu. Musisz stworzyć nową klasę, zaimplementować IValueConverter
lub IMultiValueConverter
odrzucić wartość lub wartości z object
właściwego typu, przetestować DependencyProperty.Unset
(przynajmniej dla konwerterów wielowartościowych), napisać logikę konwersji, zarejestrować konwerter w słowniku zasobów [patrz aktualizacja poniżej ] i na koniec podłącz konwerter przy użyciu dość pełnego XAML (co wymaga użycia magicznych ciągów zarówno dla powiązań, jak i nazwy konwertera[patrz aktualizacja poniżej]). Proces debugowania nie jest też piknikiem, ponieważ komunikaty o błędach są często tajemnicze, szczególnie w trybie projektowania Visual Studio / Expression Blend.
Nie oznacza to, że alternatywą - uczynienie modelu widoku odpowiedzialnym za całą konwersję wartości - jest ulepszenie. Mogłoby to być kwestią zieleni po drugiej stronie. Oprócz utraty eleganckiego rozdzielenia problemów, musisz napisać kilka pochodnych właściwości i upewnić się, że sumiennie dzwonisz RaisePropertyChanged(() => DerivedProperty)
przy ustawianiu podstawowych właściwości, co może okazać się nieprzyjemnym problemem konserwacji.
Poniżej znajduje się wstępna lista zalet i wad umożliwiania modelom widoku obsługi logiki konwersji i rezygnacji z konwerterów wartości:
- Plusy:
- Mniej łącznych wiązań, ponieważ wyeliminowano wiele konwerterów
- Mniej magicznych ciągów (ścieżki wiązania
+ nazwy zasobów konwertera) Koniec rejestrowania każdego konwertera (plus utrzymanie tej listy)- Mniej pracy do napisania każdego konwertera (nie wymaga interfejsów implementacyjnych ani rzutowania)
- Może łatwo wstrzykiwać zależności, aby pomóc w konwersji (np. Tabele kolorów)
- Znaczniki XAML są mniej szczegółowe i łatwiejsze do odczytania
- Ponowne użycie konwertera jest nadal możliwe (choć wymagane jest pewne planowanie)
- Żadnych tajemniczych problemów z DependencyProperty.Unset (problem zauważyłem w przypadku konwerterów wielowartościowych)
* Przekreślenia wskazują korzyści, które znikają, jeśli używasz rozszerzeń znaczników (patrz aktualizacja poniżej)
- Cons:
- Silniejsze sprzężenie między modelem widoku a widokiem (np. Właściwości muszą uwzględniać pojęcia takie jak widoczność i pędzle)
- Więcej całkowitych właściwości, aby umożliwić bezpośrednie mapowanie dla każdego powiązanego widoku
(patrz Aktualizacja 2 poniżej)RaisePropertyChanged
należy wywołać dla każdej właściwości pochodnej- Musi nadal polegać na konwerterach, jeśli konwersja jest oparta na właściwości elementu interfejsu użytkownika
Tak więc, jak zapewne możesz powiedzieć, mam zgagę dotyczącą tego problemu. Bardzo waham się pójść drogą refaktoryzacji tylko po to, by zdać sobie sprawę, że proces kodowania jest równie nieefektywny i żmudny, niezależnie od tego, czy używam konwerterów wartości, czy ujawniam wiele właściwości konwersji wartości w moim modelu widoku.
Czy brakuje mi zalet / wad? Dla tych, którzy wypróbowali oba sposoby przeliczania wartości, które według ciebie działały dla Ciebie lepiej i dlaczego? Czy są jakieś inne alternatywy? (Uczniowie wspominali coś o dostawcach deskryptorów typów, ale nie mogłem zrozumieć, o czym rozmawiali. Doceniłbym każdy wgląd w to).
Aktualizacja
Dowiedziałem się dzisiaj, że można użyć czegoś, co nazywa się „rozszerzeniem znaczników”, aby wyeliminować potrzebę rejestrowania konwerterów wartości. W rzeczywistości nie tylko eliminuje to konieczność ich rejestrowania, ale w rzeczywistości zapewnia inteligencję do wybierania konwertera podczas pisania Converter=
. Oto artykuł, który mnie zaczął: http://www.wpftutorial.net/ValueConverters.html .
Możliwość użycia rozszerzenia znaczników nieco zmienia równowagę w mojej liście zalet i wad oraz powyższej dyskusji (patrz przekreślenia).
W wyniku tego odkrycia eksperymentuję z systemem hybrydowym, w którym używam konwerterów BoolToVisibility
i tego, co nazywam, MatchToVisibility
oraz modelu widoku dla wszystkich innych konwersji. MatchToVisibility to w zasadzie konwerter, który pozwala mi sprawdzić, czy wartość powiązana (zwykle wyliczenie) odpowiada jednej lub większej liczbie wartości określonych w XAML.
Przykład:
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
Zasadniczo polega to na sprawdzeniu, czy status ma status Zakończony lub Anulowany. Jeśli tak, to widoczność zostaje ustawiona na „Widoczne”. W przeciwnym razie ustawi się na „Ukryty”. Okazało się to bardzo częstym scenariuszem i posiadanie tego konwertera zapisało mi około 15 właściwości w moim modelu widoku (plus powiązane instrukcje RaisePropertyChanged). Pamiętaj, że po wpisaniu Converter={vc:
„MatchToVisibility” pojawia się w menu inteligencji. To znacznie zmniejsza ryzyko wystąpienia błędów i sprawia, że korzystanie z konwerterów wartości jest mniej uciążliwe (nie musisz zapamiętywać ani szukać nazwy żądanego konwertera wartości).
Jeśli jesteś ciekawy, wkleję poniższy kod. Jedną ważną cechą tej realizacji MatchToVisibility
jest to, że sprawdza, czy wartość związana jest enum
, a jeśli jest, to kontrole, aby upewnić się Value1
, Value2
itp są także teksty stałe tego samego typu. Zapewnia to sprawdzenie w czasie projektowania i w czasie wykonywania, czy któraś z wartości wyliczenia jest błędnie wpisana. Aby poprawić to do sprawdzania czasu kompilacji, możesz zamiast tego użyć następującego (napisałem to ręcznie, więc proszę wybacz mi, jeśli popełniłem jakieś błędy):
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}}"
Chociaż jest to bezpieczniejsze, jest po prostu zbyt gadatliwe, aby było dla mnie tego warte. Równie dobrze mogę po prostu użyć właściwości modelu widoku, jeśli mam to zrobić. W każdym razie stwierdzam, że kontrola czasu projektowania jest całkowicie adekwatna do scenariuszy, które próbowałem do tej pory.
Oto kod dla 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;
}
}
Oto kod dla 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();
}
}
Oto metoda rozszerzenia ToEnum
public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
Aktualizacja 2
Od kiedy opublikowałem to pytanie, natknąłem się na projekt open source, który używa „tkania IL” do wstrzykiwania kodu NotifyPropertyChanged dla właściwości i właściwości zależnych. To sprawia, że wdrożenie wizji modelu widoku Josha Smitha jako „konwertera wartości na sterydach” jest absolutną bryzą. Możesz po prostu użyć „Właściwości automatycznie zaimplementowane”, a tkacz zrobi resztę.
Przykład:
Jeśli wprowadzę ten kod:
public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
... to się kompiluje:
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);
}
}
To ogromna oszczędność w ilości kodu, który musisz wpisać, odczytać, przewinąć, itp. Co ważniejsze, jednak oszczędza ci to konieczności zastanawiania się, jakie są twoje zależności. Możesz dodawać nowe „właściwości” jak FullName
bez starannego wchodzenia w łańcuch zależności w celu dodawania RaisePropertyChanged()
wywołań.
Jak nazywa się ten projekt typu open source? Oryginalna wersja nosi nazwę „NotifyPropertyWeaver”, ale właściciel (Simon Potter) stworzył platformę o nazwie „Fody” do obsługi całej serii tkaczy IL. Odpowiednik NotifyPropertyWeaver na tej nowej platformie nazywa się PropertyChanged.Fody.
- Instrukcje konfiguracji Fody: http://code.google.com/p/fody/wiki/SampleUsage (zamień „Virtuosity” na „PropertyChanged”)
- Witryna projektu PropertyChanged.Fody: http://code.google.com/p/propertychanged/
Jeśli wolisz korzystać z NotifyPropertyWeaver (który jest nieco prostszy w instalacji, ale niekoniecznie będzie aktualizowany w przyszłości poza poprawkami błędów), oto strona projektu: http://code.google.com/p/ powiadomienie
Tak czy inaczej, te rozwiązania tkacza IL całkowicie zmieniają rachunek w debacie między modelem widoku na sterydach a przetwornikami wartości.
MatchToVisibility
wydaje się być wygodnym sposobem na włączenie niektórych prostych przełączników trybu (mam jeden widok, w szczególności z toną części, które można włączać i wyłączać. W większości przypadków sekcje widoku są nawet oznaczone (za pomocą x:Name
), aby pasowały do trybu odpowiadają.) Tak naprawdę nie przyszło mi do głowy, że jest to „logika biznesowa”, ale dam wam komentarz.
BooleanToVisibility
pobiera jedną wartość związaną z widocznością (prawda / fałsz) i tłumaczy ją na inną. To wydaje się idealnym zastosowaniemValueConverter
. Z drugiej stronyMatchToVisibility
koduje logikę biznesową wView
(jakie rodzaje elementów powinny być widoczne). Moim zdaniem logikę tę należy sprowadzić doViewModel
, a nawet dalej, do tego, co nazywamEditModel
. To, co użytkownik może zobaczyć, powinno być testowane.