WPF – Caliburn.Micro – jeszcze trochę o bindowaniu

W poprzednim poście pokazałem prostą aplikację z wykorzystaniem Caliburn.Micro. Teraz chciałem pokazać bardziej zaawansowane (ale tylko trochę) sposoby bindowania.

Bindowanie bezpośrednio do obiektu

W przykładzie, który został pokazany uzupełniając dane w textboxach przypisujemy te dane do odpowiedniej property w klasie. Fragment dla przypomnienia

public string LastName
{
    get { return _person.LastName; }
    set
    {
        _person.LastName = value;
        NotifyOfPropertyChange("LastName");
        NotifyOfPropertyChange(() => CanSayHi);
    }
}

Dla prostych przykładów ma to jeszcze jakiś sens, ale najczęściej zadania, którymi się zajmujemy nie są proste. Miałem kiedyś przygotować okno umożliwiające podgląd i ewentualne poprawienie danych. Dane były zaczytywane z xmla. W jednym oknie było do wyświetlenia blisko 70 textboxów. 70 textboxów, których wartość użytkownik mógł sobie edytować. Tylko, że edycja była dość specyficzna. Tzn. były textboxy, które były “teoretycznie” sumą pozostałych. Dlatego napisałem “teoretycznie” ponieważ mogło się zdarzyć, że należało wpisać tam wartość większą niż suma składników(ale nigdy nie mniejszą). Były też takie textboxy, które stanowiły sumę tych sum (czasem ściśle a czasem według tych samych co wcześniej zasad). Niektóre textboxy nie mogły przyjmować wartości ujemnych, a inne musiały stanowić określony procent innych – procent w przedziale określonym w przepisach z wartością pobieraną z konkretnego textboxa (przy okazji pozdrowienia dla autorów tych cudownych przepisów). Zgodnie z tym co napisałem wcześniej należałoby w naszym ViewModel napisać 70 property (w stylu LastName) i dość skomplikowaną metodę sprawdzającą czy dane są poprawne. Byłoby to ewidentne złamanie zasady SRP (np. nowe pola w xmlu, nowe zasady walidacji – ustawodawca dba o to aby nie zabrakło nam pracy). Jak więc pogodzić te dwa sprzeczne ze sobą założenia? W tym celu zamiast 70 property mam w ViewModelu tylko jedną property.
Zróbmy więc zmiany w dotychczasowym projekcie. Kod dostępny na githubie pod tagiem DeepPropertyBinding.

private Person _person;
public Person Ctx
{
    get { return _person; }
    set
    {
        _person = value;
        NotifyOfPropertyChange("Ctx");
        NotifyOfPropertyChange(() => CanSayHi);
    }
}

W skorelowanym z ViewModelem View również należy dokonać zmian. Zmiany te są proste i w gruncie rzeczy niewielkie.
Otóż zamiast:

<TextBox x:Name="LastName" />

Należy wpisać

<TextBox x:Name="Ctx_LastName" />

Informujemy w ten sposób, że bindujemy textbox do property LastName obiektu Ctx (znak _ zastępuje niedozwoloną w tym miejscu kropkę).
Co jednak w sytuacji gdybyśmy chcieli zejść głębiej. Wyświetlić jakąś zagnieżdżoną property. Dla lepszego zobrazowania zmodyfikowałem w tym celu klasę Person. Kod dostępny na githubie pod tagiem DeepPropertyBinding_2ndLevel.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
   
    public Person()
    {
        Address = new Address();
    }
}

Kod klasy Address:

public class Address
{
    public string PostCode { get; set; }
    public string Town { get; set; }
}

Następnie zmodyfikowałem widok tak aby można było zapisać od razu kod pocztowy i miejscowość dla danej osoby:

<TextBox x:Name="Ctx_FirstName" />
<TextBox x:Name="Ctx_LastName" />
<TextBox x:Name="Ctx_Address_PostCode" />
<TextBox x:Name="Ctx_Address_Town" />

i to działa.

Mater Detail View

Przejdźmy teraz sytuacji trochę bardziej skomplikowanej. Chcemy wyświetlać listę osób i mieć możliwość edycji wybranej osoby (czyli klasyczny widok master detail). Kod dostępny na githubie pod tagiem MasterDetail.
W tym celu na widoku potrzebujemy kontrolki typu ItemsControl (np. DataGrid,ComboBox, ListBox).

<ListBox x:Name="Customers">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding FirstName}" />
                <TextBlock Text="  " />
                <TextBlock Text="{Binding LastName}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

W ViewModelu tworzymy property typu BindableCollection ( w tym konkretnym przypadku typu Person).

public BindableCollection<Person> Customers
{
    get { return _customers; }
    set
    {
        _customers = value;
        NotifyOfPropertyChange("Customers");
    }
}

Ponieważ nazwa property to Customers to tworzę property z nią powiązaną SelectedCustomer:

public Person SelectedCustomer
{
    get { return _customer; }
    set
    {
        _customer = value;
        NotifyOfPropertyChange("SelectedCustomer");
    }
}

W xamlu ustawiamy x:Name wspomnianej wcześniej kontrolki na Customers a szczegóły wyświetlamy w sposób opisany powyżej (wykorzystując SelectedCustomer i znak _ do wejścia do określonej property).
Stosując domyślną konwencję musimy po prostu dodać Selected przed nazwą naszej kolekcji (jeśli kończy się ona na s to oczywiście to s należy pominąć). Jeśli więc naszą kolekcje nazwiemy Customers to wybrany z niej element to SelectedCustomer (tak samo jeśli kolekcję nazwiemy przez pomyłkę Customer !) jeśli z kolei nazwiemy naszą kolekcję Klienci to wybrany z niej element to SelectedKlienci! Z mojego punktu widzenia nazwy angielskie sprawdzają się tutaj lepiej (no może z wyjątkiem People bo SelectedPeople raczej średnio wygląda).
Na zakończenie dzisiejszej części jeszcze taka mała uwaga dotycząca BindableCollection. Jeśli ktoś jest już zaznajomiony z MVVM to zapewne kojarzy ObservableCollection. BindableCollection jest to kolekcja specyficzna dla Caliburn.Micro dziedzicząca z ObservableCollection. Należy używać BindableCollection dlatego, że jest ona thread-safe.
W następnej części zapowiadana na dzisiaj obsługa zdarzeń.

11 thoughts on “WPF – Caliburn.Micro – jeszcze trochę o bindowaniu

  1. Pingback: dotnetomaniak.pl
  2. Wartościowy artykuł, ogólnie cała seria ciekawa.

    Co do małej niedogodności przy ‘People’ – spokojnie można użyć formy ‘Persons’, wtedy mamy ‘SelectedPerson’ i po problemie. ‘Persons’ jest często używane, nawet Longman dopuszcza użycie takiej formy (w pewnych przypadkach, np gdy nie znamy tożsamości danej grupy osób).

    Pozdrawiam.

  3. Dzięki za kolejny interesujący post. W wolnej chwili sam zabiorę sie za Caliburn;) zaciekawiły mnie Twoje posty:)

  4. Co zrobić, jeżeli dla któregoś pola chcemy zaimplementować pewną logikę ? Np w wyniku
    modyfikacji pola, w innym ma się coś zadziać ? Nie rozjedzie się to jeżeli w set coś się dopisze ?

    np w polu PostCode:

    public string PostCode{ get; set { SetTown() ;}}

    Zadziała coś takiego ? Czy trzeba z poziomu widoku wszystko obsługiwać?

    1. Zadziała, ale nie tak od razu. Kod powinien wyglądać mniej więcej

      private string _postCode;
      public string PostCode
      {
          get { return _postCode; }
          set
          {
              _postCode = value;
              OnPropertyChanged("PostCode");
              SetTown();
          }
      }
      
      private string _town;
      public string Town
      {
          get { return _town; }
          set
          {
              _town = value;
              OnPropertyChanged("Town");
          }
      }
      

      Klasa Address powinna implementować INotifyPropertyChanged.

      1. Dzięki. Czyli mniej więcej podobnie jak w PropertyChanged.Fody. Niemniej jednak Caliburn jednak wydaje się wygodniejszy 🙂

    1. Całkiem zwyczajnie

      <TextBlock Text="{Binding PropertyToConvert, Converter={StaticResource myConverterKey}}" />
      
  5. A jeśli załóżmy chciałbym bindować do właściwości LastName obiektu Person ( jak w Pana przykładzie “Ctx_LastName”) to jak wtedy użyć konwertera? Wiem że w tym przypadku nie ma takiej konieczności. ale chodzi mi tylko o przykład.

    1. Najprościej w ten sposób

      <TextBlock Text="{Binding Ctx.LastName, Converter={StaticResource lastNameConverter}}"/>
      

      Tylko że tracimy binding przez x:Name.
      Drugim rozwiązaniem które mi przychodzi na myśl to wstrzyknięcie konwersji do ViewModelu. View tylko wyświetla dane a ViewModel pobiera dane z Modelu (albo wysyła do). Tworzymy tylko klasę PersonDTO, która będzie pełnić rolę pośrednika pomiędzy naszym modelem a ViewModelem. Proces przepisywania danych z Person na PersonDTO (wraz z naszą konwersją) można zautomatyzować np. Automapperem.
      Każde z rozwiązań ma swoje wady i zalety. Najlepiej spróbować oba i zdecydować, które w danym momencie lepiej nam pasuje. Jeśli chodzi o sens konwersji LastName to może to być np. wyświetlanie samych inicjałów.

Comments are closed.