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.
źródło
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.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.Odpowiedzi:
ValueConverters
W niektórych przypadkach używałem logiki,ViewModel
aw innych logikę . Mam wrażenie, żeValueConverter
staje się częściąView
warstwy, więc jeśli logika jest naprawdę częścią,View
to umieść ją tam, w przeciwnym razie umieść ją wViewModel
.Osobiście nie widzę problemu z
ViewModel
radzeniem sobie zeView
specyficznymi pojęciami, takimi jakBrush
es, ponieważ w moich aplikacjachViewModel
istnieje tylko jako testowalna i wiążąca powierzchnia dlaView
. Jednak niektórzy ludzie wkładają dużo logiki biznesowej wViewModel
(ja nie) i w tym przypadkuViewModel
jest to bardziej część ich warstwy biznesowej, więc w tym przypadku nie chciałbym tam rzeczy specyficznych dla WPF.Wolę inną separację:
View
- Rzeczy WPF, czasem nie do przetestowania (jak XAML i kodowanie), ale takżeValueConverter
sViewModel
- testowalna i dająca się powiązać klasa, która jest również specyficzna dla WPFEditModel
- część warstwy biznesowej, która reprezentuje mój model podczas manipulacjiEntityModel
- część warstwy biznesowej, która reprezentuje mój model jako utrwalonyRepository
- odpowiedzialny za trwałośćEntityModel
bazy danychWięc sposób, w jaki to robię, nie ma większego sensu dla
ValueConverter
sUciekłem od niektórych twoich „oszustów”, czyniąc moje
ViewModel
bardzo ogólnym. Na przykład jeden,ViewModel
który mam, o nazwie,ChangeValueViewModel
implementuje właściwość Label i właściwość Value. Na stronieView
znajduje sięLabel
powiązanie z właściwością Label orazTextBox
powiązanie z właściwością Value.Następnie mam klucz,
ChangeValueView
który jestDataTemplate
wyłączony z tegoChangeValueViewModel
typu. Ilekroć WPF widzi, żeViewModel
to stosujeView
. Mój konstruktorChangeValueViewModel
przyjmuje logikę interakcji, której potrzebuje, aby odświeżyć swój stan odEditModel
(zwykle po prostu przekazując aFunc<string>
) i działania, które musi podjąć, gdy użytkownik edytuje wartość (tylko ten,Action
który wykonuje pewną logikę wEditModel
).Element nadrzędny
ViewModel
(dla ekranu) pobieraEditModel
konstruktor i tworzy po prostu odpowiednie elementy elementarne,ViewModel
takie jakChangeValueViewModel
. Ponieważ rodzicViewModel
wstrzykuje akcję, która ma zostać wykonana, gdy użytkownik dokona jakiejkolwiek zmiany, może przechwycić wszystkie te akcje i wykonać inne. Dlatego wstrzyknięta akcja edycjiChangeValueViewModel
może wyglądać następująco:Oczywiście
foreach
pętlę można zrefaktoryzować gdzie indziej, ale wystarczy wykonać działanie, zastosować ją do modelu, a następnie (zakładając, że model zaktualizował swój stan w jakiś nieznany sposób), mówi wszystkim dzieciom,ViewModel
aby poszły i odzyskały swój stan model ponownie. Jeśli stan się zmienił, są oni odpowiedzialni za wykonanie swoichPropertyChanged
zdarzeń, jeśli to konieczne.To całkiem ładnie radzi sobie z interakcją między, na przykład, listą i panelem szczegółów. Gdy użytkownik wybierze nowy wybór, aktualizuje ten
EditModel
wybór iEditModel
zmienia wartości właściwości ujawnionych dla panelu szczegółów. TeViewModel
dzieci, które są odpowiedzialne za wyświetlanie informacji szczegół panel automatycznie otrzymywać powiadomienia, że muszą sprawdzić nowe wartości, a jeśli już zmieniony, ogień oni swojePropertyChanged
wydarzenia.źródło
ViewModel
warstwie. Nie wszyscy się ze mną zgadzają, ale zależy to od tego, jak działa twoja architektura.CalendarViewModel
dlaCalendarView
UserControl lub aDialogViewModel
dla aDialogView
). To tylko moja opinia :)ViewModel
.Jeśli konwersja ma coś wspólnego z widokiem, na przykład decyduje o widoczności obiektu, określa obraz do wyświetlenia lub zastanawia się, jakiego koloru pędzla użyć, zawsze umieszczam konwertery w widoku.
Jeśli jest to związane z biznesem, takie jak ustalenie, czy pole powinno być zamaskowane, lub jeśli użytkownik ma uprawnienia do wykonania akcji, konwersja nastąpi w moim ViewModel.
Z twoich przykładów, myślę, że tracisz duży kawałek WPF:
DataTriggers
. Wygląda na to, że używasz konwerterów do określania wartości warunkowych, ale konwertery powinny naprawdę służyć do konwersji jednego typu danych na inny.W twoim przykładzie powyżej
Chciałbym użyć a,
DataTrigger
aby określić, który obraz ma zostać wyświetlony, a nieConverter
. Konwerter służy do konwersji jednego typu danych na inny, a wyzwalacz służy do określania niektórych właściwości na podstawie wartości.Jedynym przypadkiem, w którym rozważę użycie Konwertera, jest to, czy powiązana wartość faktycznie zawierała dane obrazu, a ja musiałem przekonwertować ją na typ danych zrozumiały dla interfejsu użytkownika. Na przykład, jeśli źródło danych zawiera właściwość o nazwie
ImageFilePath
, to rozważę użycie Konwertera do konwersji ciągu zawierającego lokalizację pliku obrazu na taki,BitmapImage
który mógłby być użyty jako źródło mojego obrazuW rezultacie mam jedną przestrzeń nazw bibliotek pełną ogólnych konwerterów, które konwertują jeden typ danych na inny, i rzadko muszę kodować nowy konwerter. Są chwile, kiedy będę chciał konwerterów dla określonych konwersji, ale są one na tyle rzadkie, że nie mam nic przeciwko ich pisaniu.
źródło
Grid
elementy. Próbuję również wykonywać takie czynności, jak ustawianie pędzli dla pierwszego planu / tła / obrysu w oparciu o dane w moim modelu widoku i określoną paletę kolorów zdefiniowaną w pliku konfiguracyjnym. Nie jestem pewien, czy to świetnie pasuje do wyzwalacza lub konwertera. Jedynym problemem, jaki mam do tej pory z umieszczeniem większości logiki widoku w modelu widoku, jest połączenie wszystkichRaisePropertyChanged()
połączeń.DataTrigger
, nawet wyłączając elementy Grid. Zazwyczaj umieszczam miejsce, wContentControl
którym powinna znajdować się moja dynamiczna treść, i wymieniamContentTemplate
element uruchamiający. Mam przykład pod poniższym linkiem, jeśli jesteś zainteresowany (przewiń w dół do sekcji z nagłówkiemUsing 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}}"
i<TextBlock Text="I'm a Business" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Business}}"
Zależy to od tego, co testujesz.
Bez testów: zmieszaj Wyświetl kod z ViewModel do woli (zawsze możesz później refaktoryzować).
Testy na ViewModel i / lub niższym: użyj konwerterów.
Testy na warstwach modelu i / lub niższych: intermix Wyświetl kod z ViewModel do woli
ViewModel streszcza Model dla Widoku . Osobiście użyłbym ViewModel dla pędzli itp. I pominąłem konwertery. Testuj na warstwie (warstwach), w której dane są w „ najczystszej ” formie (tj. Warstwach modelu ).
źródło
Visibility
,SolidColorBrush
iThickness
.Prawdopodobnie nie rozwiąże to wszystkich problemów, o których wspomniałeś, ale należy wziąć pod uwagę dwie kwestie:
Najpierw musisz umieścić kod konwertera gdzieś w pierwszej strategii. Czy bierzesz pod uwagę tę część widoku lub model widoku? Jeśli jest to część widoku, dlaczego nie umieścić właściwości specyficznych dla widoku w widoku zamiast modelu widoku?
Po drugie, wygląda na to, że Twój projekt bez konwertera próbuje zmodyfikować właściwości istniejących obiektów, które już istnieją. Wygląda na to, że już zaimplementowali INotifyPropertyChanged, więc dlaczego nie skorzystać z utworzenia obiektu opakowania specyficznego dla widoku, z którym można się połączyć? Oto prosty przykład:
źródło
Czasami dobrze jest użyć konwertera wartości, aby skorzystać z wirtualizacji.
Przykładem tego jest projekt, w którym musieliśmy wyświetlać dane w postaci maski bitowej dla setek tysięcy komórek w siatce. Kiedy dekodowaliśmy maski bitów w modelu widoku dla każdej pojedynczej komórki, ładowanie programu trwało zbyt długo.
Ale kiedy stworzyliśmy konwerter wartości, który dekodował pojedynczą komórkę, program ładował się w ułamku czasu i był tak samo responsywny, ponieważ konwerter jest wywoływany tylko wtedy, gdy użytkownik patrzy na określoną komórkę (i trzeba go było tylko wywołać maksymalnie trzydzieści razy za każdym razem, gdy użytkownik zmieni widok na siatce).
Nie wiem, jak MVVM skarżyło się na to rozwiązanie, ale skróciło czas ładowania o 95%.
źródło