wpfmvvmmvvm-lightexpression-blendeventtocommand

WPF How to add blend interaction trigger to style resource


I am using VS 2012, with WPF 4.5

I want to be able to add a blend interaction trigger to a style resource so that I can have that defined in one place (resource dictionary) and use in many places throughout my app.

Specifically, I want to use the EventToCommand that comes with the MVVM-Light framework and have it inserted into a textbox style and attached to the LostFocus event of the textbox. I am planning on using this to mark certain textboxes with a ValidationStyle that triggers a bound command (in a viewmodel) to the LostFocus event of the Textbox. This validation style will use the IDataErrorInfo to display errors to the user through the UI.

This question is similar to the following questions (but they do not have the total solution):

EventToCommand in button style

How to add a Blend Behavior in a Style Setter

QUESTION: How can I add a blend EventToCommand to the textbox lostfocus that is bound to a command in the viewmodel datacontext (I don't want to use code behind or attached property, I want it to be totally defined in XAML)?


Solution

  • So I must admit that I had a working answer when I wrote this, but it took me a long time to figure it out so I am posting it here hoping it helps someone else even though it is a very specific scenario.

    I am using the MVVM model for my application so I don't want to have code behind the xaml pages. I also wanted a way to have a textbox bind to the IDataErrorInfo properties where the validation for that textbox is triggered through the lostfocus event of the textbox. This event will be bound to a relay command on the viewmodel that will validate the applicable object and add realted errors.

    So i needed to have the textbox lostfocus eventcommand take the textbox name (which matches the column names from the database) as a command parameter.

    Here is a screen shot of what I am trying to accomplish WPF Screen Shot of Textbox Validation STyle

    Here is how I did it:

    First I defined the command on the view model:

    Imports GalaSoft.MvvmLight.Command
    Private _LostFocusValidateCommand As RelayCommand(Of String)
    
        Public ReadOnly Property LostFocusValidateCommand() As RelayCommand(Of String)
            Get
                If _LostFocusValidateCommand Is Nothing Then
                    _LostFocusValidateCommand = New RelayCommand(Of String)(AddressOf LostFocusValidateExecute)
                End If
                Return _LostFocusValidateCommand
            End Get
        End Property
        Private Sub LostFocusValidateExecute(sParam As String)
            NewClient.PropertyValitaion(False, sParam)
        End Sub
    

    here is the property validation using IDataErrorInfo (I left out he basic implementation of IDataErrorInfo to save space, leave a comment if you want me to post it)

     Public Sub PropertyValitaion(bAllProperties As Boolean, Optional sProperty As String = "")
        'initialize validation helper
        Dim vhelper As New ValidationHelper
    
        If bAllProperties Or sProperty = "chrCompany" Then
            If String.IsNullOrEmpty(chrCompany) Then
                AddError("chrCompany", "You must enter a Company Name")
            Else
                RemoveError("chrCompany")
            End If
        End If
        If bAllProperties Or sProperty = "chrFirst" Then
            If String.IsNullOrEmpty(chrFirst) Then
                AddError("chrFirst", "You must enter a First Name")
            Else
                RemoveError("chrFirst")
            End If
        End If
        If bAllProperties Or (sProperty = "chrPhone1" Or sProperty = "chrPhone1Ext") Then
            If String.IsNullOrEmpty(Trim(chrPhone1Ext)) = False And String.IsNullOrEmpty(Trim(chrPhone1)) Then
                Me.AddError("chrPhone1", "Provide a phone number or remove extension")
            Else
                RemoveError("chrPhone1")
            End If
            If String.IsNullOrEmpty(Trim(chrPhone1)) = False Then
                If vhelper.CheckPhoneNumber(Me.chrPhone1) = False Then
                    Me.AddError("chrPhone1", "Phone 1 format invalid")
                Else
                    RemoveError("chrPhone1")
                End If
            End If
        End If
    
    End Sub
    

    The hard part was figuring out how to define the style. The style is long, sorry, the joys of "readable" xml:

        <Style x:Key="FTC_ValidateTextBox" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
        <Style.Setters>
            <Setter Property="FontFamily" Value="Open Sans Condensed"/>
            <Setter Property="FontSize" Value="19" />
            <Setter Property="Margin" Value="3,3,15,6"/>
            <Setter Property="Padding" Value="10,3"/>
            <Setter Property="TextWrapping" Value="Wrap" />
            <Setter Property="HorizontalAlignment" Value="Stretch" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="Background" Value="{StaticResource DetailTextBox}" />
            <Setter Property="BorderBrush" Value="{StaticResource MediumGray}" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="Foreground" Value="Black" />
            <Setter Property="AllowDrop" Value="true"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border Name="Bd" SnapsToDevicePixels="true" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                                <i:Interaction.Triggers>
                                    <i:EventTrigger EventName="LostFocus">
                                        <cmd:EventToCommand Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},Path=DataContext.LostFocusValidateCommand}"
                                                            CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}},Path=Name}"/>
                                    </i:EventTrigger>
                                </i:Interaction.Triggers>
                            </ScrollViewer>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <Border BorderBrush="{StaticResource MediumRed}" >
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="Auto" />
                                </Grid.RowDefinitions>
                                <AdornedElementPlaceholder Name="parentTextBox" />
                                <TextBlock Grid.Row="1" Style="{StaticResource FTC_DetailError}"
                                           Text="{Binding ElementName=parentTextBox, Path=AdornedElement.(Validation.Errors).CurrentItem.ErrorContent}"/>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style.Setters>
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
                <Setter Property="BorderBrush" Value="{StaticResource MediumRed}"/>
                <Setter Property="Foreground" Value="{StaticResource MediumRed}"/>
                <Setter Property="Margin" Value="3,3,15,31"/>
            </Trigger>
        </Style.Triggers>
    </Style>
    
    <Style x:Key="FTC_DetailError" TargetType="TextBlock">
        <Style.Setters>
            <Setter Property="FontFamily" Value="Open Sans Condensed"/>
            <Setter Property="Control.FontWeight" Value="Light" />
            <Setter Property="Foreground" Value="{StaticResource TitleWhite}"/>
            <Setter Property="FontSize" Value="15" />
            <Setter Property="Margin" Value="0"/>
            <Setter Property="Padding" Value="10,3"/>
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="Background" Value="{StaticResource MediumRed}"/>
        </Style.Setters>             
    </Style>
    

    all the magic happens in the property template. THe following must be included in the top declarations of your resource dictionary:

    > xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    > xmlns:cmd="http://www.galasoft.ch/mvvmlight"
    

    all the magic happens in the template property that defines the control template. You can not wrap a i:interaction in the control template itself, it must be contained within a derived object, almost anything really, border, scrollviewer, wrappanel etc... Then you set the vent trigger and the command properties. They should be easy enough to follow, I pass the textbox name as the command parameter. The client "box" you see in the screen shot is a grid with its data context set to a new client object property of the parent viewmodel. SO in order to access the command in the parent viewmodel, I had to reference the parent's datacontext and call the command property.

    Again, I realize that this is a very specific scenario, but I thought it has some examples that might be able to help others. I am now able to define one style for all textboxes in the application that are data-entry and that I want to trigger basic validation procedures. It will save me having to define the custom command behaviour on all those text boxes individually, and this is all accomplished in xaml, with out code behind.

    Cheers