Archive

Posts Tagged ‘Exceptions’

Commanding II: Beyond Calling Methods

June 30, 2012 Leave a comment

In the last post about Commanding I described a basic technique for binding RoutedCommands to method calls in the ViewModel. Today I want to introduce some refinements which make the user experience more complete: Status messages for running commands, localizing exception messages, and how to have one status bar display the status messages of multiple commands.

The complete example code for this post is contained in the CommandingExample project which is part of the WPFGluePublished download available through the downloads page.

Status Messages

If users click a button or a menu item in order to execute a command, they expect instant feedback on whether the command is executing. If the result is not visible immediately, they will appreciate a status message somewhere in the UI which tells them that the command has been executed, and also a message that tells them when it has finished.

In order to accommodate this scenario, we will add three more properties to our Command base class:

Public Shared ReadOnly ReadyMessageProperty As DependencyProperty = DependencyProperty.Register("ReadyMessage", GetType(String), GetType(Command))
Public Property ReadyMessage As String
    Get
        Return GetValue(ReadyMessageProperty)
    End Get
    Set(ByVal value As String)
        SetValue(ReadyMessageProperty, value)
    End Set
End Property

Public Shared ReadOnly WorkingMessageProperty As DependencyProperty = DependencyProperty.Register("WorkingMessage", GetType(String), GetType(Command))
Public Property WorkingMessage As String
    Get
        Return GetValue(WorkingMessageProperty)
    End Get
    Set(ByVal value As String)
        SetValue(WorkingMessageProperty, value)
    End Set
End Property

Private Shared ReadOnly StatusPropertyKey As DependencyPropertyKey = DependencyProperty.RegisterReadOnly("Status", GetType(String), GetType(Command), New PropertyMetadata())
Public Shared ReadOnly StatusProperty As DependencyProperty = StatusPropertyKey.DependencyProperty
Public Property Status As String
    Get
        Return GetValue(StatusProperty)
    End Get
    Private Set(ByVal value As String)
        SetValue(StatusPropertyKey, value)
    End Set
End Property

 

WorkingMessage and ReadyMessage are supposed to be configured by the developer. They are the messages which are shown to the user when the command starts executing and when it is finished. Status is a readonly property. This is where the messages arrive. The idea is to set Status to the WorkingMessage before one calls the ViewModel, and to the ReadyMessage when the ViewModel method has returned.

However, simply changing the property won’t get the message displayed: data binding, notifying of changes that require a new rendering, and executing the ViewModel method all happen on the same thread, so the changed status messages will not be processed until when the ViewModel method has returned. In order to give the UI time to render the WorkingMessage, we need to use an asynchronous pattern including the Dispatcher:

Protected Overridable Sub OnExecuted(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)
    Exception = Nothing
    IsRunning = True
    Status = WorkingMessage
    Dim a As Func(Of Object, Object, Exception) = AddressOf Me.DoExecute
    Dim op As System.Windows.Threading.DispatcherOperation = Me.Dispatcher.BeginInvoke(a, Windows.Threading.DispatcherPriority.Background, sender, e.Parameter)
    op.Wait()
    Dim success As Boolean = (op.Result Is Nothing)
    If success Then
        Status = ReadyMessage
    Else
        Dim ex As Exception = op.Result
        Status = ex.Message
        Exception = ex
    End If
    IsRunning = False
End Sub

Protected Overridable Function DoExecute(ByVal sender As Object, ByVal parameter As Object) As Exception
    Dim result As Exception = Nothing
    Try
        Execute(sender, parameter)
    Catch ex As Exception
        result = ex
    End Try
    Return result
End Function

 

The OnExecuted method begins with setting some properties that indicate that the command is about to execute. The status message is one of them. Then, it calls Dispatcher.Invoke with the address of a wrapper function, DoExecute, which calls the ViewModel method. The Dispatcher.Invoke call includes a DispatcherPriority.Background parameter. This tells the Dispatcher that it should finish all its usual housekeeping chores, among others displaying our status message, before it calls the DoExecute method. Dispatcher.Invoke returns a DispatcherOperation object. Using this object, we first call Wait in order to yield control to the Dispatcher. The Dispatcher will then take care of everything that is more important (again including displaying our status message), and finally call our method. When the method has returned, the Wait method will return, and the DispatcherOperation’s Result property will contain the return value of DoExecute.

Localizing Exception Messages

The main purpose of DoExecute is to catch any exceptions that might be thrown by the ViewModel method before they reach the Dispatcher, and to expose them through the Command’s Exception property. By default, in case of an exception, the Status property will return the Exception’s Message.

However, this message might not be suitable for display to the users: it might be in a language different form the UI language, or it might be formulated in a way that makes sense to developers, but not to end users who lack the necessary technical background.

So, the UI should be able to receive Exceptions and to translate them into localized messages. Since there might be important information embedded in the original exception message, there should also be a way to extract this information and to insert it into the translated message.

The idea here is to create a converter which receives an Exception, and can be configured in order to create a String message.

The converter looks like this:

Public Class ErrorMessageConverter
    Inherits Freezable
    Implements IValueConverter

    Public Sub New()
        MyBase.New()
        ErrorMessages = New ErrorMessageCollection
    End Sub

    Protected Overrides Function CreateInstanceCore() As System.Windows.Freezable
        Return New ErrorMessageConverter
    End Function

    Private Shared ReadOnly ErrorMessagesPropertyKey As DependencyPropertyKey = DependencyProperty.RegisterReadOnly("ErrorMessages", GetType(ErrorMessageCollection), GetType(ErrorMessageConverter), New PropertyMetadata())
    Public Shared ReadOnly ErrorMessagesProperty As DependencyProperty = ErrorMessagesPropertyKey.DependencyProperty
    Public Property ErrorMessages As ErrorMessageCollection
        Get
            Return GetValue(ErrorMessagesProperty)
        End Get
        Private Set(ByVal value As ErrorMessageCollection)
            SetValue(ErrorMessagesPropertyKey, value)
        End Set
    End Property

    Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
        Dim result As String = Nothing
        If value IsNot Nothing Then
            For Each em As ErrorMessage In ErrorMessages
                result = em.GetMessage(value)
                If result IsNot Nothing Then
                    Exit For
                End If
            Next
        End If

        Return result
    End Function

    Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
        Throw New NotSupportedException
    End Function
End Class

Public Class ErrorMessage
    Inherits Freezable

    Protected Overrides Function CreateInstanceCore() As System.Windows.Freezable
        Return New ErrorMessage
    End Function

    Public Shared ReadOnly ExceptionProperty As DependencyProperty = DependencyProperty.Register("Exception", GetType(String), GetType(ErrorMessage), New PropertyMetadata("Exception"))
    Public Property Exception As String
        Get
            Return GetValue(ExceptionProperty)
        End Get
        Set(ByVal value As String)
            SetValue(ExceptionProperty, value)
        End Set
    End Property

    Private Shared ReadOnly MessageParserProperty As DependencyProperty = DependencyProperty.Register("MessageParser", GetType(System.Text.RegularExpressions.Regex), GetType(ErrorMessage))
    Private Property MessageParser As System.Text.RegularExpressions.Regex
        Get
            Return GetValue(MessageParserProperty)
        End Get
        Set(ByVal value As System.Text.RegularExpressions.Regex)
            SetValue(MessageParserProperty, value)
        End Set
    End Property

    Public Shared ReadOnly MessageRegExProperty As DependencyProperty = DependencyProperty.Register("MessageRegEx", GetType(String), GetType(ErrorMessage), New PropertyMetadata(AddressOf OnMessageRegExChanged))
    Public Property MessageRegEx As String
        Get
            Return GetValue(MessageRegExProperty)
        End Get
        Set(ByVal value As String)
            SetValue(MessageRegExProperty, value)
        End Set
    End Property
    Private Shared Sub OnMessageRegExChanged(ByVal d As ErrorMessage, ByVal e As DependencyPropertyChangedEventArgs)
        Try
            d.MessageParser = New System.Text.RegularExpressions.Regex(e.NewValue)
        Catch ex As Exception
            d.MessageParser = Nothing
        End Try
    End Sub

    Public Shared ReadOnly FormatProperty As DependencyProperty = DependencyProperty.Register("Format", GetType(String), GetType(ErrorMessage))
    Public Property Format As String
        Get
            Return GetValue(FormatProperty)
        End Get
        Set(ByVal value As String)
            SetValue(FormatProperty, value)
        End Set
    End Property

    Public Function IsMatch(ByVal ex As Exception) As Boolean
        Dim result As Boolean = ex IsNot Nothing
        If result Then
            result = ex.GetType.FullName.EndsWith(Exception)
        End If
        If result And MessageParser IsNot Nothing Then
            result = MessageParser.IsMatch(ex.Message)
        End If
        Return result
    End Function

    Public Function GetMessage(ByVal ex As Exception) As String
        Dim result As String = Nothing
        If IsMatch(ex) Then
            If MessageParser Is Nothing Then
                result = Format
            Else
                Dim m As System.Text.RegularExpressions.Match = MessageParser.Match(ex.Message)
                Dim values() As String = (From g As System.Text.RegularExpressions.Group In m.Groups
                                          Select g.Value).ToArray
                result = String.Format(Format, values)
            End If
        End If
        Return result
    End Function
End Class

Public Class ErrorMessageCollection
    Inherits FreezableCollection(Of ErrorMessage)
End Class

 

The most important element here is the ErrorMessage class. The ErrorMessage class has the following properties:

  • Exception: The type name of the Exception class, or at least the last part of it.
  • MessageRegEx: optionally, a standard .Net regular expression which must match the original exception message. It can contain grouping constructs (i.e. parentheses) in order to extract information from the exception message.
  • Format: the String into which the exception message should be translated. This string can contain format placeholders (like {1}) for the information extracted from the original exception message.
    If the converter receives an exception, it iterates the configured ErrorMessage objects in order to find the first one that matches the exception. Then it calls GetMessage on this object in order to return a message, and stops the iteration. Since the Exception property only matches the last part of the exception’s type name, can configure a catchall using Exception=”Exception” on the last ErrorMessage object in your ErrorMessageConverter.

Here an example for how to configure an ErrorMessageConverter:

<l:ErrorMessageConverter x:Key="errorMessageTranslator">
    <l:ErrorMessage Exception="InvalidOperationException"
                    MessageRegEx="This operation is ([^!]*)!" Format="Don't try {1} operations!"/>
</l:ErrorMessageConverter>

 

Notice that the regular expression’s first capture group has index 1, while index 0 will return the complete match.

Creating a Status Line

While we can now display status messages for one command, normally one doesn’t want one status control for each command that might be executed through the UI. Instead, normally there is a status line that displays status of any command that might be executing at the moment.

So, what we need is kind of a funnel which collects status messages from all commands and always returns the most recent message. The component which does this is called FunnelConverter:

Public Class FunnelConverter
    Implements IMultiValueConverter

    Private _Values() As Object
    Private _Result As Object = Nothing

    Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert
        Dim result As Object = Nothing

        If values Is Nothing OrElse values.Length = 0 Then
            _Values = Nothing
        Else
            Dim i As Integer = 0
            Do While _Values IsNot Nothing AndAlso (i < _Values.Length And i < values.Length And result Is Nothing)
                If _Values(i) Is Nothing OrElse Not _Values(i).Equals(values(i)) Then
                    result = values(i)
                End If
                i += 1
            Loop
            Do While result Is Nothing And i < values.Length
                result = values(i)
                i += 1
            Loop
            _Values = values.ToArray
        End If

        If result IsNot Nothing Then
            _Result = result
        End If
        Return _Result

    End Function

    Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
        Throw New NotSupportedException
    End Function

 

This class is a MultiBindingConverter. It is supposed to be used with a MultiBinding, which accepts an array of bindings, and returns a new value whenever one of the bound values changes. The FunnelConverter stores a copy of the list of values it receives, and displays the first value that has changed, or the first value which is not nothing if there are no values, yet. Since all command status changes are executed on the UI thread, only one value will change at any given time, and no changes will be lost. Since this converter stores the results of the last invocation, one has to make sure that each converter instance is only used in one binding; so, the most natural solution is to not declare the converter as a resource, but to instantiate it inline in the MultiBinding tag:

<StatusBarItem>
    <StatusBarItem.Content>
        <MultiBinding>
            <MultiBinding.Converter>
                <c:FunnelConverter/>
            </MultiBinding.Converter>
            <Binding Source="{StaticResource commands}" Path="Commands[Open].Status"/>
            <Binding Source="{StaticResource commands}" Path="Commands[Find].Status"/>
            <Binding Source="{StaticResource commands}" Path="Commands[Print].Status"/>
            <Binding Source="{StaticResource commands}" Path="Commands[Stop].Status"/>
            <Binding Source="{StaticResource commands}" Path="Commands[Print].Exception" Converter="{StaticResource errorMessageTranslator}"/>
        </MultiBinding>
    </StatusBarItem.Content>
</StatusBarItem>

Conclusion

Using the techniques described in this post, one can keep the users informed about what the application is currently doing, and about any exceptions that might occur in ViewModel code. Since all messages which are configured in XAML can be localized, using the classes described here is also a step towards globalizing the application.

Another topic I want to cover in a later post is how to have commands executed asynchronously in the background, either on a background thread or by executing a number of short operations on the Dispatcher.

Advertisements