Category Archives: WPF

WPF: TextBlock vs. Label

Na pierwszy rzut oka, TextBlock oraz Label są bardzo podobne. Obydwie kontrolki(?)  wyświetlają tekst. Różnice są jednak spore i postaram się je wyjaśnić. Sprawdźmy co następujący kod wyświetli:

<StackPanel>
   <Label>Label</Label>
   <TextBlock>TextBlock</TextBlock>
</StackPanel>

image

Kontrolka Label jest lekko przesunięta w prawo. Jest to spowodowane faktem, że Padding dla Label domyślnie ma wartość 5. W przypadku TextBlock jest to 0, dlatego też na powyższym screenie TextBlock przylega do krawędzi.

Ustawmy IsEnabled StackPanel’a na false i sprawdźmy jak zachowają się kontrolki:

<StackPanel IsEnabled="False">
   <Label>Label</Label>
   <TextBlock>TextBlock</TextBlock>
</StackPanel>

image

Label zachował się jak zwykła kontrolka, kolor został ustawiony na szary. W przypadku TextBlock nic nie zostało zmienione.

Kolejną zaletą Label jest wsparcie dla tzw. Access Keys. Dzięki nim użytkownik naciskając Alt oraz specjalnie oznaczoną literę z  Label Content, może przejść do sąsiadującego TextBox np.

<StackPanel>
   <Label Target="{Binding ElementName=textBox}">La_bel</Label>
   <TextBox x:Name="textBox"></TextBox>

   <TextBox></TextBox>
   <TextBox></TextBox>
</StackPanel>

Jeśli użytkownik naciśnie Alt + b, focus zostanie ustawiony na TextBox (textBox). Literę należy poprzedzić znakiem _.

Teraz przyszedł czas na wady Label. Jeśli zajrzymy na MSDN to dowiemy się, że TextBlock nie jest tak naprawdę kontrolką ponieważ dziedziczy z FrameworkElement. Label z kolei dziedziczy z ContentControl (dlatego zawiera właściwość Content). Z tego powodu Label jest znacznie cięższą kontrolką i bardziej obciążającą zasoby systemu. Stosujmy zatem Label w przypadku formularzy jako etykieta pól edycyjnych. Jeśli chcemy wyświetlić większy fragment tekstu, wtedy lepszym rozwiązaniem jest TextBlock. Warto również zwrócić uwagę na to, że TextBlock jest wykorzystywany jako składowa różnych kontrolek np. Button. Przeładowując więc globalnie style dla Textblock, zmienimy również wygląd m.in. button’ów.

PropertyChanged bez przekazywania string’a

Często musimy implementować interfejs INotifyPropertyChanged. Szczególnie w przypadku wykorzystania wzorca MVVM oraz WPF. Najczęściej programiści wykorzystują podstawową implementację i przekazują nazwę parametru jako czysty string np:

if(PropertyChanged!=null)
{
    PropertyChanged(this,new PropertyChangedEventArgs("propName"));
}

Wszystko działa bardzo dobrze, dopóki nie zmienimy nazwy właściwości. Przede wszystkim należy wtedy pamiętać o zaktualizowaniu wszystkich wywołań ProperyChanged. Nie możemy ponadto skorzystać ze standardowego narzędzia do refaktoryzacji dostarczonego przez Visual Studio – string nie zostanie zmieniony. Istnieje jednak bardzo proste rozwiązanie, polegające na wykorzystaniu wyrażeń Lambda. Najlepiej rozważmy od razu fragment kodu:

public static class INotifyPropertyChangedExtensions
{
   public static void Raise<T>(this PropertyChangedEventHandler handler,object sender, Expression<Func<T>> expression)
   {
       if (handler != null)
       {
           var body = propertyExpression.Body as MemberExpression;
           if (body == null)
               throw new ArgumentException("'expression' should be a member expression");

           var expression = body.Expression as ConstantExpression;
           if (expression == null)
               throw new ArgumentException("'expression' body should be a constant expression");
   
           var e = new PropertyChangedEventArgs(body.Member.Name);
           handler(sender, e);
       }
   }      
}

Powyższa klasa to rozszerzenie do ProperyChangedEventHandler. Sprawdza ona czy jest podpięte jakieś zdarzenie, jeśli tak to odpala je przekazując dynamicznie skonstruowany PropertyChangedEventArgs. Teraz zamiast przekazywać nazwę właściwości w formie stringu, przekazujemy po prostu samą właściwość:

PropertyChanged.Raise(this,()=>this.Price);

WPF, dynamiczne i statyczne menu

Ostatnio napotkałem problem stworzenia menu zawierającego zarówno dynamiczne elementy (binding) jak i statyczne. Okazało się, że wcale nie jest to takie proste jak to jest w przypadku większości rzeczy w WPF. Zacznijmy jednak od przedstawienia sposobów tworzenia menu w WPF. Pierwszy to oczywiste statyczne menu, w całości zdefiniowane w XAML:

<Menu Height="23" Name="menu1" Width="200">
    <MenuItem Header="Plik">
        <MenuItem Header="statyczny tekst 1"/>
        <MenuItem Header="statyczny tekst 2"/>
        <MenuItem Header="statyczny tekst 3"/>
    </MenuItem>
</Menu>

W powyższym kodzie nie ma nic nadzwyczajnego. Sytuacja jest również łatwa gdy chcemy w całości zdefiniować menu jako dynamiczne:

<Menu Height="23" Name="menu1" Width="200">
   <MenuItem Header="Plik" ItemsSource="{Binding Items}">        
       <MenuItem.ItemTemplate>
           <DataTemplate>
               <TextBlock Text="{Binding DisplayName}" />
           </DataTemplate>
       </MenuItem.ItemTemplate>
       <MenuItem.ItemContainerStyle>
           <Style TargetType="MenuItem">
               <Setter Property="Command" Value="{Binding ShowCmd}" />
           </Style>
       </MenuItem.ItemContainerStyle>
   </MenuItem>
</Menu>

Gdzie pojedynczy item to:

public class Item
{
   public string DisplayName { get; set; }
   public ICommand ShowCmd { get; set; }
}

Co jednak w przypadku gdy chcemy aby menu Plik zawierało część statycznych podmenu zdefiniowanych w XAML a część pochodziła z kolekcji (na dodatek z różnych kolekcji)? Sytuacja się trochę komplikuje ale możemy rozwiązań problem za pomocą CollectionContainer:

<Menu Height="23" Name="menu1" Width="200">
   <Menu.Resources>
       <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>            
   </Menu.Resources>
   
   <MenuItem Header="Plik">        
       <MenuItem.ItemsSource>
           <CompositeCollection>
               <MenuItem Header="Statyczny element 1"/>
               <MenuItem Header="Statyczny element 2"/>
               <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>               
           </CompositeCollection>
       </MenuItem.ItemsSource>
   </MenuItem>
   
</Menu>

Przede wszystkim przenieśliśmy elementy do ItemsSource. CompositeCollection pozwala zdefiniować kolekcje menu składającej się z statycznych i dynamicznych elementów. Dynamiczne pozycje zdefiniowane są za pomocą CollectionContainer, który wykorzystuje jako źródło danych CollectionViewSource, który z kolei powiązany jest z Items. Po uruchomieniu powyższego przykładu zobaczymy menu składające się z różnych submenu ale niestety musimy jeszcze określić jaka właściwość powinna być wyświetlana oraz jaka komenda z klasy Item powinna zostać powiązana:

<MenuItem Header="Plik">            
  <MenuItem.Resources>
      <Style TargetType="{x:Type MenuItem}">
          <Style.Triggers>
              <DataTrigger Binding="{Binding Converter={StaticResource objectToTypeConverter}}" Value="{x:Type itemsNamespace:Item }">
                  <Setter Property="Header" Value="{Binding DisplayName}"/>
                  <Setter Property="Command" Value="{Binding ShowCmd}"/>
              </DataTrigger>
          </Style.Triggers>
      </Style>
      <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>
  </MenuItem.Resources>
  <MenuItem.ItemsSource>
      <CompositeCollection>                    
          <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>
          <MenuItem Header="Statyczny element 1"/>
          <MenuItem Header="Statyczny element 2"/>
      </CompositeCollection>
  </MenuItem.ItemsSource>
</MenuItem>

Za pomocą DataTrigger sprawdzamy czy typ DataContext jest równy typeof(Item). Jeśli tak to znaczy, że jest to element dynamiczny powiązany z kolekcją Items i możemy ustawić dowolne właściwości(w tym komendę i header). Na zakończenie konwerter objectToTypeConverter:

public class ObjectToTypeConverter:IValueConverter
{
   #region IValueConverter Members

   public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       if (value == null)
           return null;
       else
           return value.GetType();
   }

   public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       throw new NotImplementedException();
   }

   #endregion
}

Nieobsłużone wyjątki w WPF

Domyślnie WPF wyświetla okno dialogowe nieobsłużonego błędu a następnie po przyciśnięciu przycisku aplikacja zostaje zamknięta. W poście zajmiemy jednak się przeładowaniem tego zachowania poprzez np. zapis błędu do pliku. W WPF możemy wykorzystać zdarzenie DispatcherUnhandledException(plik App.xaml.cs), która wywoływana jest dla nieobsłużonych wyjątków. Jeśli zatem w kodzie znajdzie się wywołanie throw bez klauzuli catch, zdarzenie DispatcherUnhandledException zostanie uruchomione. Przykład, plik app.xaml.cs

public partial class App : Application
{
    private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        // zapis do logów, wyświetlanie własnego okna błędów itp.
        e.Handled = true;
    }
}

app.xaml:

<Application x:Class="WpfApplication2.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             DispatcherUnhandledException="App_DispatcherUnhandledException"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

Ustawienie wartości e.Handled na true powoduje, że wyjątek zostanie potraktowany jako obsłużony. Jest to więc doskonałe miejsce na wyświetlenie wszelkich customowych okien dialogowych czy umieszczenie po prostu logger’a. W pliku XAML podpinamy tylko zdarzenie (DispatcherUnhandledException ).

Niestety powyższy kod będzie działał wyłącznie w sytuacji gdy to główny wątek wyrzuca wyjątek. Dla wszelkich innych wątków (np. w BackgroundWorker, Thread itp), metoda NIE zostanie wywołana. Jeśli zatem chcemy dodać obsługę wyjątków wyrzuconych w zewnętrznych wątkach, musimy:

  1. obsłużyć wyjątek w zewnętrznym wątku (catch),
  2. przekazać wyjątek poza wątek (czyli do głównego wątku)
  3. rethrow – wyrzucić ponownie wyjątek – w tym momencie zostanie już uruchomiona metoda App_DisptacherUnhandledException.

ContextMenu, DataContext oraz problemy z binding

Jakiś czas temu napotkałem na problem podczas próby przypisania własnego kontekstu kontrolce ContextMenu. Zacznijmy może od razu od przykładu XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding SimpleViewModel}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

Prosty widok  – zwykły Grid z kontekstowym menu. DataContext ustawiam na SimpleVIewModel a następnie binduje właściwość Text. Code-behind wygląda następująco:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        SimpleViewModel = new WpfApplication1.SimpleViewModel();
        DataContext = this;
        InitializeComponent();
    }
    public SimpleViewModel SimpleViewModel { get; private set; }
}
public class SimpleViewModel
{
    public string Text { get { return "Hello"; } }
}

Kod chyba nie wymaga komentarza. Po uruchomieniu aplikacji spodziewałem się, że w menu zostanie wyświetlony tekst “Hello”. Niestety ku mojemu zdziwieniu żaden tekst nie pojawił się. Po krótkim research’u  okazało się, że ContextMenu nie jest w WPF składową drzewa elementów (visual tree) i wszelkie bindingi (nawet te z ElementName) nie zadziałają. DataContext jest ustawiany na początku na kontekst rodzica i nie można go w podany sposób przeładować.

Istnieje na szczęście pewne, eleganckie obejście problemu. Otóż ContextMenu posiada właściwość PlacementTarget, które wskazuje na obiekt na którym znajduje się aktualnie menu (w tym przypadku Grid). Za pomocą PlacementTarget można zatem dostać się do DataContext rodzica a następnie do SimpleViewModel! Oto przykład:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding PlacementTarget.DataContext.SimpleViewModel,RelativeSource={RelativeSource Self}}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

RelativeSelf ustawia jako kontekst obiekt kontrolki – uzyskujemy więc dostęp do wszelkich publicznych właściwości kontrolki (a więc również do PlacementTarget). Następnie poprzez DataContext dostajemy się do naszego SimpleViewModel. Po uruchomieniu aplikacji, przekonamy się, że teraz wiązanie danych zostało przeprowadzone poprawnie.

Binding zdarzeń w WPF, MVVM cz.2

Kilka miesięcy temu pisałem o attached behaviour jako sposobie na wiązanie zdarzeń do komend. Sposób całkowicie poprawny i wygodny w użyciu. Dzisiaj jednak chciałbym przedstawić nieco prostsze rozwiązanie z wykorzystaniem bibliotek z Expression Blend SDK.

Jeśli jeszcze nie posiadacie SDK możecie je ściągnąć ze strony Microsoft’u. Do projektu dołączamy  bibliotekę System.Windows.Interactivity (SDK). Następnie w pliku XAML spróbujmy powiązać zdarzenie MouseMove z komendą ShowMsgCmd:

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <i:InvokeCommandAction Command="{Binding ShowMsgCmd}"/>                    
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Namespace “i” powinien być skojarzony z biblioteką System.Windows.Interactivity:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Jak widać Expression Blend dostarcza pewnie rozszerzenie dla wyzwalaczy. Możemy w ten sposób wykonać własny kod w momencie odpalenia wyzwalacza (który jest uruchamiany w tym przypadku na podstawie zdarzenia). W EventTrigger definiujemy własne akcje – klasy, które dziedziczą po TriggerAction. InvokeCommandAction dziedziczy po TriggerAction i wywołuje wskazaną komendę. Możemy nawet podać jakiś argument do komendy:

<Button>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <i:InvokeCommandAction Command="{Binding ShowMsgCmd}" CommandParameter="argument"/>                    
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>

Niestety często potrzebować będziemy argumentów zdarzenia (EventArgs). Aktualna implementacja InvokeCommandAction nie pozwala na to. Aby móc w obsłudze (Execute) korzystać z EventArgs musimy zaimplementować własny TriggerAction:

public sealed class CustomCommandInvoker : TriggerAction<DependencyObject>
    {                    
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
            "Command", typeof(ICommand), typeof(CustomCommandInvoker), null);
    
        public ICommand Command
        {
            get
            {
                return (ICommand)this.GetValue(CommandProperty);
            }
            set
            {
                this.SetValue(CommandProperty, value);
            }
        }                
        protected override void Invoke(object parameter)
        {

            if (this.AssociatedObject != null)
            {
                ICommand command = Command;
                if ((command != null) && command.CanExecute(parameter))
                {
                    command.Execute(parameter);
                }
            }
        }
    }

Jak widać, implementacja nie jest skomplikowana. Zastosowanie wygląda bardzo podobnie:

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <customInvokers:CustomCommandInvoker Command="{Binding ShowMsgCmd}"/>                    
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Następnie w obsłudze komendy możemy wykorzystać EventArgs dostarczane przez zdarzenia:

private void ShowMsg(object args)
{
    MouseEventArgs mouseArgs = (MouseEventArgs)args;
    MessageBox.Show(string.Format("Hello world! {0}:{1}", mouseArgs.GetPosition(this).X, mouseArgs.GetPosition(this).Y));
}

Na zakończenie warto dodać, że biblioteka z ExpressionBlend nie jest “ciężka” – zawiera dosłownie kilka klas. Dodając ją do projektu nie musimy się martwić o to, że dodamy wiele niepotrzebnych innych klas.

Message Boxes w MVVM

Wzorzec Model-View-ViewModel jest najczęściej wykorzystywany przez programistów WPF. Związane jest to z mechanizmem wiązań, który znacząca ułatwia wdrożenie MVVM do projektu. ViewModel stanowi wyeksponowany model przeznaczony do bindingu, z kolei widok to zwykły, pasywny plik XAML. Ważną cechą ViewModel jest brak powiązań z interfejsem użytkownika. Najlepiej aby projekt zawierający ViewModel nie posiadał żadnych referencji do bibliotek warstwy prezentacji.

Częstym zadawanym pytaniem jest: Jak wywoływać standardowe okna komunikatów  (MessageBox)? Aby poprawnie zaimplementować VM, nie powinno się wywoływać komunikatów bezpośrednio VM. Umieszczenie wywołań w Code-Behind również nie jest dobrym podejściem (często messagebox zależą od logiki biznesowej oraz nie powinniśmy umieszczać kodu w code-behind).

Najlepszym rozwiązaniem jest stworzenie usługi odpowiedzialnej za wyświetlanie komunikatów a następnie załadowanie jej z kontenera. Interfejs reprezentujący takową usługę mógłby wyglądać następująco:

interface IMessageBoxService
{
    Result Show(string message);
}

Z kolei przykładowe implementacje:

class RealMessageBoxService: IMessageBoxService
{
    public Result Show(string message)
    {
        MessageBox.Show(message);
        return Result.OK // w tym miejscu powinnismy zmapowac wynik zwrocony przez MessageBox do wlasnego enum.
    }
}
class MockBoxService: IMessageBoxService
{
    public Result Show(string message)
    {
        return Result.OK;
    }
}

Następnie w ViewModel powinniśmy skorzystać z kontenera (IoC, np. UnityContainer) aby uzyskać instancję usługi:

IMessageBoxService service = container.Resolve<IMessageBoxService>();

Dzięki wstrzyknięciu zależności, możemy w łatwy sposób przełączać się między prawdziwą implementacją wyświetlającą komunikat a zwykłym stubem. Podsumowując z przedstawionego w tym poście podejścia zyskujemy:

  1. Niezależność od bibliotek warstwy prezentacji (System.Windows itp.)
  2. TESTOWALNOŚĆ – wyświetlając komunikaty bezpośrednio w VM pozbawieni jesteśmy możliwości skorzystania z testów jednostkowych (testy jednostkowe wymagające interakcji użytkownika?- zaprzeczenie głównym zasadom). W przypadku usług, możemy wstrzyknąć zwykły MOCK object i przetestować kod – nawet w przypadku skomplikowanych worflow’ów w których logika biznesowa zależy od interakcji użytkownika.
  3. Jedno miejsce w kodzie w którym można umieszczać metody odpowiedzialne za wyświetlanie najpopularniejszych okien dialogowych. W końcu można stworzyć dodatkowe metody typu ShowWelcomeText itp. w klasie implementującej usługę.