Pokazywanie postów oznaczonych etykietą WP7. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą WP7. Pokaż wszystkie posty

niedziela, 10 czerwca 2012

Windows Phone - wyświetlanie błędów walidacji

Kilka miesięcy temu w tym poście http://premium-hands.blogspot.com/2011/11/walidacja.html przedstawiałem różne sposoby walidacji jakie są dostępne na platformie Silverlight. Jako, że ostatnio mam więcej czasu aby pisać jakieś proste aplikacje pod Windows Phonem potrzebowałem mechanizmu walidacji działającego pod tą platformą. Według MSDN-u wszystkie metody walidacji przedstawione we wspomnianym wcześniej poście powinny działać. Niestety po przerzuceniu mechanizmu z Silverlighta na Windows Phona okazało się, że błędy walidacji nie zostają wyświetlone na interfejsie. Po przeszukaniu kilku for oraz blogów dowiedziałem się, że przyczyną takiego stanu rzeczy jest brak obsługi reakcji na błędy w templacie TextBoxów (i innych elementów typu input). Zatem aby wyświetlić błędy walidacji, pozostaje nam zmodyfikować istniejący template TextBox-a. Zacznijmy od wyciągnięcia standardowego templatu TextBox-a z Windows Phona. W tym celu możemy użyć aplikacji Expression Blend. Otwórzmy w niej jakikolwiek projekt typu Windows Phone Application, w którym na kontrolce mamy naniesionego Textboxa. W moim przypadku, po wczytaniu projektu do Blenda ekran wygląda w następujący sposób
Następnie zaznaczamy w Blendzie TextBox-a i z opcji znajdujących się w lewym górnym rogu aktualnej zakładki wybieramy TextBox->Edit Template-> Edit Copy.
W oknie, które się pojawi klikamy OK. Następnie możemy przejść do widoku XAML-a, w sekcji phone:PhoneApplicationPage.Resources powinien pojawić się nam nowy styl wyglądający w ten sposób
<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
   <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
  </ControlTemplate>
<Style x:Key="TextBoxStyle1" TargetType="TextBox">
   <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
   <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
   <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
   <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
   <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
   <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
   <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
   <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
   <Setter Property="Padding" Value="2"/>
   <Setter Property="Template">
    <Setter.Value>
     <ControlTemplate TargetType="TextBox">
      <Grid Background="Transparent">
       <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
         <VisualState x:Name="Normal"/>
         <VisualState x:Name="MouseOver"/>
         <VisualState x:Name="Disabled">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
         <VisualState x:Name="ReadOnly">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
        </VisualStateGroup>
        <VisualStateGroup x:Name="FocusStates">
         <VisualState x:Name="Focused">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
         <VisualState x:Name="Unfocused"/>
        </VisualStateGroup>
       </VisualStateManager.VisualStateGroups>
       <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
        <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
       </Border>
       <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
        <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
       </Border>
      </Grid>
     </ControlTemplate>
    </Setter.Value>
   </Setter>
  </Style>
W ten oto sposób udało nam się wydobyć domyślny styl oraz template TextBoxa w Windows Phonie. W kolejnym kroku musimy zmodyfikować ten template w taki sposób, aby zaczął reagować na zmiany stanu walidacji. Zacznijmy od sprawdzenia jakie stany wizualne może obsługiwać TextBox. W tym celu musimy przeglądnąć metadane TextBoxa - możemy to zrobić klikając PPM na klasie TextBox-a i wybierając Go To Definition. Naszym oczom powinno ukazać się coś takiego.
Widzimy, że klasa TextBox jest opatrzona atrybutami TemplateVisualState, nas interesują najbardziej trzy z tych atrybutów:
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "Valid")]
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "InvalidFocused")]
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "InvalidUnfocused")]
Mówią nam one, że w przypadku gdy wystąpią błędy walidacji nasza kontrolka przechodzi w stan InvalidFocused lub InvalidUnfocused. Natomiast, w przypadku gdy nie ma błędów znajduje się ona w stanie Valid. Analizując kod domyślnego templatu TextBoxa widzimy, że stany z grupy ValidationStates nie są obsługiwane - czas zatem to zmienić. Po pierwsze do templatu musimy dorzucić elementy, które będą wyświetlały błędy walidacji. Zmieńmy zatem opakowujący wszystko element Grid na StackPanel, a następnie dodajmy element typu TextBlock oraz Border. Elementy wyświetlające błędy walidacji musimy umieścić w jednym kontenerze z elementami odpowiadającymi za wyświetlanie tekstu. Zatem musimy je opakować w Grida. Po zastosowaniu powyższych wskazówek nasz template będzie wyglądał w następujący sposób
<StackPanel>
...
...
...
 <Grid>
   <Grid.RowDefinitions>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
   </Grid.RowDefinitions>
 <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
       <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch"   Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
 </Border>
  <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
      <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
 </Border>
     <Border x:Name="ValidationBorder" Grid.RowSpan="2" BorderBrush="Red" BorderThickness="2" Visibility="Collapsed"  >
 </Border>
     <TextBlock x:Name="ValidationMessage" Foreground="Red" FontSize="16" Visibility="Collapsed" Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"  Padding="0,0,0,2" Margin="10,-14,0,0"  />
</Grid>
</StackPanel>
Dodanie elementów wyświetlających błędy to nie wszystko, musimy jeszcze określić kiedy nasze elementy mają być widoczne. W chwili obecnej ich właściwość Visibility jest ustawiona na Collapsed. Zatem elementy te nie będą widoczne, jak również nie będą zajmowały miejsca na interfejsie. Jak już wcześniej wspomniałem kontrolka TextBoxa w przypadku wystąpienia błędów walidacji przejdzie do stanów InvalidFocused lub InvalidUnfocused. Pozostaje nam zatem zareagować na przejście kontrolki w te stany i odpowiednio zmodyfikować właściwość Visibility bordera oraz textblocka. W tym celu posłużymy się VisualStateManagerem oraz animacjami. Do naszego templatu dodajmy następujący wpis
<VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidUnfocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
W ten oto sposób zdefiniwaliśmy w XAML-u grupę (VisualStateGroup ) o nazwie ValidationStates - zauważmy, że nazwa grupy jest taka sama jak nazwa grupy z atrybutów klasy TextBox. Dodaliśmy również trzy stany wizualne (VisualState), których nazwy również odpowiadają nazwą zdefiniowanym w atrubytach klasy TextBox. Od tej chwili gdy kontrolka TextBoxa będzie posiadała błędy walidacji, zostanie odpalona animacja zmieniająca właściwość Visibility bordera "ValidationBorder" oraz TextBlocka "ValidationMessage" na Visible. Ostatecznie cały gotowy template będzie wyglądał w następujący sposób
<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
        <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
    </ControlTemplate>
    <Style TargetType="TextBox">
        <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
        <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
        <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
        <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
        <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
        <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
        <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
        <Setter Property="Padding" Value="2"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TextBox">
                    <StackPanel Background="Transparent">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver"/>
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="ReadOnly">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="FocusStates">
                                <VisualState x:Name="Focused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Unfocused"/>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidUnfocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="Auto"></RowDefinition>
                            </Grid.RowDefinitions>
                            <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
                                <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch"   Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
                            </Border>
                            <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
                                <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
                            </Border>
                            <Border x:Name="ValidationBorder" Grid.RowSpan="2" BorderBrush="Red" BorderThickness="2" Visibility="Collapsed"  >
                            </Border>
                            <TextBlock x:Name="ValidationMessage" Foreground="Red" FontSize="16" Visibility="Collapsed" Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"  Padding="0,0,0,2" Margin="10,-14,0,0"  />

                        </Grid>

                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Tak natomiast przedstawiają się błędy walidacji wyświetlone na TextBoxach

Windows Phone - własny DataTemplateSelector

Pisząc moją małą aplikację pod Windows Phone po raz kolejny natknąłem się na problem. Pod WP7 nie ma tak przydatnej rzeczy jaką jest DataTemplateSelector znany nam bardziej, lub mniej z Silverlighta oraz WPF-a.Na szczęście napisane własnego DataTemplateSelector-a nie jest specjalnie skomplikowane. Jak zwykle w takich przypadkach liczy się pomysł - jak dobrze, że jest Google. Nasz customowy DataTemplateSelector zostanie oparty o kontrolkę ContentControl. Po pierwsze stwórzmy klasę bazową DataTemplateSelectora dziedziczącą po ContentControl,w której przeciążamy funkcję OnContentChanged,
public abstract class AbstractDataTemplateSelector : ContentControl
    {
        public abstract DataTemplate SelectTemplate(object item, DependencyObject container);
        
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            ContentTemplate = SelectTemplate(newContent, this);
        }
    }
Klasa ta będzie klasą z której będą wywodzić się wszystkie nasze TemplateSelectory. Załóżmy, że chcemy zbudować DataTemplateSelector, który będzie obsługiwał aplikację chata. Powinien on zatem reagować na dwa typy elementów:
  • wiadomości przychodzące
  • wiadomości wychodzące
Stwórzmy zatem "wyspecjalizowany" DataTemplateSelector, który w zależności od typu wiadomości zwróci odpowiedni DataTemplate.
public class ChatMessageTemplateSelector : AbstractDataTemplateSelector
    {
        public DataTemplate Out
        {
            get;
            set;
        }

        public DataTemplate In
        {
            get;
            set;
        }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            ChatMessage message = item as ChatMessage;
            if (message != null)
            {
                switch (message.MessageDirection)
                {
                    case MessageDirection.In:
                        return In;
                    case MessageDirection.Out:
                        return Out;
                    default:
                        return Out;
                }
            }
            return In;
        }
    }
W klasie ChatMessageTemplateSelector przeciążyliśmy metodę SelectTemplate. W metodzie tej, na podstawie zbindowanego obiektu (parametr item) określamy jaki template powinniśmy zwrócić. Templaty do wiadomości przychodzących oraz wychodzących zostały zdefiniowane jako propertisy.
  • Out - template wiadomości wychodzącej
  • In - template wiadomości przychodzącej
Pozostaje nam tylko zdefiniować wymienione wyżej templaty. Nie będziemy robić tego w kodzie, ale w XAML-u. Nasz nowo stworzony DataTemplateSelector wykorzytamy w ListBox-ie
<ListBox  ItemsSource="{Binding MessageList,Mode=TwoWay}" Grid.ColumnSpan="2" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch"
                     ItemTemplate="{StaticResource GeneralChatMessageTemplate}" >
Jako, że nasz ChatMessageTemplateSelector jest tak naprawdę obiektem typu ContentControl, zatem możemy go zbindować do właściwości ItemTemplate ListBox-a. Zasób GeneralChatMessageTemplate wygląda w następujący sposób
<DataTemplate x:Key="GeneralChatMessageTemplate">
            <Client:ChatMessageTemplateSelector HorizontalContentAlignment="Stretch" Content="{Binding}" In="{StaticResource IncomingChatMessageTemplate}" Out="{StaticResource OutcomingChatMessageTemplate}">
            </Client:ChatMessageTemplateSelector>
        </DataTemplate>
Jak widać jest to obiekt typu ChatMessageTemplateSelector, którego właściwość Content jest zbindowana do aktualnego kontekstu (pojedyncze słowo kluczowe Binding). Widzimy również, że ustawiliśmy wartości właściwości odpowiadających za templaty poszczególnych wiadomości (In="{StaticResource IncomingChatMessageTemplate}" Out="{StaticResource OutcomingChatMessageTemplate}"). Wspomniane templaty wyglądają w następujący sposób
<DataTemplate x:Key="IncomingChatMessageTemplate">
            <Border BorderThickness="0,0,0,1" BorderBrush="#2d3550" >
                <Grid Background="#181c2a" HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Border BorderThickness="1" BorderBrush="White" Margin="5,5,0,0">
                            <Image Stretch="None"  Width="30" Height="30" MaxWidth="30" MinHeight="30" MaxHeight="30"  Source="..\avatar_4934.bmp"></Image>
                        </Border>
                        <TextBlock Padding="5,5,5,0" Foreground="White" FontFamily="Segoe UI, Tahoma" FontSize="12" FontWeight="Bold" TextTrimming="WordEllipsis"  TextAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left"  Text="{Binding Sender.UserName,Mode=TwoWay}"></TextBlock>
                    </StackPanel>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="10" Padding="5,5,5,0" Foreground="#8e929f" TextTrimming="WordEllipsis"  Grid.Column="1" TextAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Text="{Binding SendTime, Mode=TwoWay, StringFormat='\{0:dd.MM HH:mm:ss\}'}"></TextBlock>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="12" Margin="5,0,0,0" TextWrapping="Wrap" Grid.ColumnSpan="2" Grid.Row="1" Text="{Binding Message,Mode=TwoWay}" ></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
<DataTemplate x:Key="OutcomingChatMessageTemplate">
            <Border BorderThickness="0,0,0,1" BorderBrush="#2d3550">
                <Grid HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Border BorderThickness="1" BorderBrush="White" Margin="5,5,0,0">
                            <Image Stretch="None"  Width="30" Height="30" MaxWidth="30" MinHeight="30" MaxHeight="30"  ></Image>
                        </Border>
                        <TextBlock Padding="5,5,5,0" Foreground="#8e929f" FontFamily="Segoe UI, Tahoma" FontSize="12" FontWeight="Bold" TextTrimming="WordEllipsis"  TextAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left"  Text="{Binding Sender.UserName,Mode=TwoWay}"></TextBlock>
                    </StackPanel>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="10" Padding="5,5,5,0" Foreground="#8e929f" TextTrimming="WordEllipsis"  Grid.Column="1" TextAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Text="{Binding SendTime, Mode=TwoWay, StringFormat='\{0:dd.MM HH:mm:ss\}'}"></TextBlock>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="12" Margin="5,0,0,0" TextWrapping="Wrap" Grid.ColumnSpan="2" Grid.Row="1" Text="{Binding Message,Mode=TwoWay}" ></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
Od teraz gdy do ListBox-a zostanie dodany nowy item, pierwszą rzeczą jaka się odpali będzie funkcja OnContentChanged. W funkcji tej AbstractDataTemplateSelector na podstawie nowo przybyłego itemu ustawi właściwość ContentTemplate, na taki template jaki zwróci mu funkcja SelectTemplate. Wynik działania ChatMessageTemplateSelector wygląda w następujący sposób

niedziela, 11 marca 2012

Prism na WP7 - Silverlight Unit Test Framework / Mocking

W poprzednim poście przedstawiłem migrację Prisma na WP7. Dziś chciałbym przetestować zmigrowany kod za pomocą testów jednostkowych dołączonych do Prisma. Bardzo fajną sesje Jeffa Wicox'a nt testów jednostkowych na WP7 możemy znaleźć pod tym linkiem. Będziemy używać Silverlight Unit Test Framework i binarek dostępnych tutaj. Przede wszystkim musimy zmigrować projekty testów do projektów Windows Phone Application. Następnie dołączamy wspomniane binarki a code behind MainPage'a powinniśmy zmienić w ten sposób:
public MainPage()
{
InitializeComponent();
Loaded += MainPage_Loaded;

}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
SystemTray.IsVisible = false;

var testPage = UnitTestSystem.CreateTestPage() as IMobileTestPage;
BackKeyPress += (x, xe) => xe.Cancel = testPage.NavigateBack();
(Application.Current.RootVisual as PhoneApplicationFrame).Content = testPage;
}

W powyższym kodzie nie dzieje się nic specjalnego. Po prostu ustawiamy stronę z poziomu której wykonujemy testy. Silverlightowa wersja testów Prisma używa Silverlight.Moq.dll. A więc, aby projekt się skompilował musimy dołączyć bibliotekę Silverlight.Moq.dll. Teraz możemy odpalić testy. Ekran powitalny wygląda w ten sposób:

Możemy odpalić wszystkie testy lub wpisać tag, który przefiltruje testy. Radzę nie uruchamiać testów w trybie debug, ponieważ jest to wyjątkowo irytujące jeżeli korzystamy z bibliotek silverlightowych, które nie wiadomo jak się zachowają (np. Silverlight.Moq.dll). Ani razu nie udało mi się dojść do końca testów, gdyż Visual wywalał się mniej więcej w połowie testowania (za chwilę poznamy tego przyczynę). Poradziłem sobie z tym uruchamiając testy bez debugowania CTRL+F5. No i wówczas otrzymałem następujący wynik:

84% testów przeszło. Wszystkie testy, które nie przeszły spowodowała biblioteka Moq, która też była odpowiedzialna za wywalanie się mojego visuala w trybie debug. Za pomocą Silverlight Unit Test Framework możemy normalnie podejrzeć szczegóły testu:

Generalnie na WP7 do tej pory nie można było korzystać z biliotek mockujących. Nie skorzystamy więc z RhinoMock lub Moq gdyż korzystają one z Reflection.Emit, który jest mocno okrojony w Mango (polecam ten topic). Jedynym rozwiązaniem jakie znalazłem jest biblioteka MoqaLate. Jest to "niby"-biblioteka pozwalająca tworzyć mocki pod Mango. Przede wszystkim MoqaLate nie jest żadnym frameworkiem tylko generatorem Mocków. Generuje klasy mocków na podstawie interfejsów projektu. Pakiet należy dodać do normalnego projektu, który będzie testowany czyli jest to w naszym przypadku Prism.Phone. Potem w zdarzeniach post build'a dodajemy linijke która odpala generator. Mocki wygenerują się w wyznaczonym folderze. Jednak jest to na tyle słabe rozwiązanie, że wywala się podczas generacji mocków prisma. Mówiąc szczerze to się zawiodłem :( Jedyne co mi zostało to stworzyć wszystko ręcznie (ale nie w tym odcinku;P). Projekt z testami dostępny jest tutaj - PRISM 4.1 / WP7.

sobota, 10 marca 2012

Prism 4.1 na Windows Phone

Zasadnicze pytanie: jaki jest sens używania Prism na Windows Phone? O zaletach prisma nie będę pisać. Jest ich wiele. Natomiast z mojego punktu widzenia sens przeniesienia Prisma na Phona jest zasadniczy: projekt wieloplatformowy oparty na Prismie - test jego wydajności, zalet i wad. Projekt jest tworzony na WP7, Silverlighta oraz WPF. Podchodząc do tematu od strony reużywania kodu i jego spójności sprawą kluczową staje się wspólny framework.

Przede wszystkim spotkałem się w necie z krytyką mówiącą, że prism na phona jest mega ciężki i lepiej korzystać z caliburn'a micro lub innych bibliotek. Ok, ok, ok... zacznijmy od początku.
Prism na phona tak naprawde nie istnieje - dlatego nie wiem skąd wzięły się opinie że jest za ciężki. W prismie na phone'a znajdziemy dosłownie 3 klasy na krzyż i brakuje tam wszystkiego dzięki czemu Prism jest używany przez wielu ludzi na świecie czyli brak IoC, modułów i regionów. Ughh... ciężki kawałek chleba. Postanowiłem zmigrować prisma silverlightowego na Phone'a. Ograniczyłem się do minimum czyli kompilowalność kodu i zachowanie elementarnej logiki.
Zaczynamy od głównego projektu. Prism.Silverlight. Przede wszystkim pozbywamy się wszelkich powiązań z kontrolką TabControl. Następnie docieramy do powiązania z klasą HashSet (której nie ma w wp7), czym się nie należy przejmować bo za chwile trzeba będzie przeorać całego ModuleManagera i HashSet będzie nie potrzebny. Zasadniczy problem przy migracji jest taki, że na WP7 nie możemy dynamicznie ładować assembly (zainteresowanych odsyłam tu i tu). Dochodzimy do funkcji LoadModuleTypes ModuleManager'a w której trzeba przyjąć że modułu nie zaciągamy z nikąd tylko on po prostu już jest. Wtedy po moich zmianach funkcja LoadModuleTypes wygląda tak:
private void LoadModuleTypes(IEnumerable moduleInfos)
{
if (moduleInfos == null)
{
return;
}

foreach (ModuleInfo moduleInfo in moduleInfos)
{
if (moduleInfo.State == ModuleState.NotStarted)
{
bool isUnavailable = Type.GetType(moduleInfo.ModuleType) == null;
if (isUnavailable)
{
throw new ModuleTypeLoadingException(string.Format
(Resources.FailedToGetType, moduleInfo.ModuleType));
}

moduleInfo.State = ModuleState.ReadyForInitialization;
}
}

this.LoadModulesThatAreReadyForLoad();
}

Jest to kluczowa zmiana w migracji. Jak widać zaciąganie modułu zamieniłem na rzucanie wyjątkiem jeżeli moduł jest niedostępny. Teraz całą resztę niepotrzebnego kodu związanego z ładowaniem modułów możemy spokojnie usunąć :) Nie będę dokładnie pisać co wywaliłem (dynamiczne ładowanie modułów i inicjalizacje modułu na żądanie). Projekt można ściągnąć i porównać kody z oryginalnym Prismem Silverlightowym.
Następnie możemy się zająć biblioteką Interactivity dla Prisma. Tutaj wystarczy wywalić wszystkie pliki powiązane z ChildWindow, który na WP7 nie jest używany (przynajmniej takie jest założenie:)
I to tak naprawdę wszystko. Dodatkowo jeżeli potrzebujemy zrobić testy jednostkowe powinniśmy zmigrować sobie Prism.Silverlight.TestSupport. Reszty bibliotek nie potrzebuję. Dodatkowo do tego zestawu wybrałem Ninject, który pomoże mi z DI. W następnym poście opisze testy jednostkowe dla WP7 na przykładzie Prisma. Cały projekt można ściągnąć pod tym linkiem

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management