Archive

Posts Tagged ‘WPF’

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

Checking Property Types Automatically

May 6, 2012 2 comments

It’s been a long time since I have written in this blog. The reason is: there have been many changes in my life during the past two years, and I simply haven’t found the time until now. Anyway, I’ve been checking out things and learning continuously, and I hope to be able to share these things more regularly in the future.

In the last post, Validating Objects with ValidationRules, I proposed a way to define ValidationRules for an object in one location and to set them on all Bindings that need them through a single attached property.

Today, I want to show how to set up basic type checks automatically. The example code is contained in the ValidationExample project in the source code distribution, which you can access through the downloads page.

How WPF handles Type

Normally, if the value entered in a TextBox cannot be converted into the type of the bound property, e.g. if you enter letters into a TextBox expecting an integer value, first of all there will be a runtime exception. By default, WPF will handle this exception, mark the TextBox as having invalid data, and assign a default “Conversion error” error message to it. Since the value cannot be converted, WPF will not update the bound property, which means that it will keep its value unchanged.

The first level of refinement here would be to allow the Binding to display exception messages by setting ValidatesOnExceptions to true. Now, if changing a property causes an exception, Validation.Errors will contain the exception’s message. Now the user will at least know whether the reason for the exception is a type mismatch or a numerical overflow.

Validation as a User Interface Responsibility

Why is this not enough?

Reusable software components like services or libraries need to check their input and throw exceptions if it is invalid. This is how one piece of software tells another piece of software that it has done something wrong.

User Interfaces, which basically translate between human users and the software they want to use, are responsible for making sure that those users only provide valid input values. The best way to do that is to offer only valid options to the user. Where that isn’t possible, validation steps in by providing explicit and meaningful validation messages which tell the users what they have to change in order to achieve the intended result.

As a rule of thumb, I’d say that one should protect one’s services and libraries by throwing exceptions, and that one should use validation in the UI to make sure that those exceptions never get thrown.

So, while exceptions are directed at technically aware developers debugging their software components, validation messages are directed at end users with possibly no technical background at all. For example, the error message you receive when you enter too many digits into the TextBox that validates with exceptions in the example project, reads: “Value was either too large or too small for an Int32”. This is obviously not what one would display to a user who has never heard of Int32.

Therefore, in the user interface, one would need two functionalities:

  1. a way to provide globalized validation messages independent of the system’s exception messages.
  2. a way to keep WPF controls from trying to update properties with values of non-matching type.

The TypeValidationRuleSetter Class

The TypeValidationRuleSetter class is a subclass of the ValidationRuleSetter class I mentioned in the last post. In addition to adding predefined ValidationRules to specified properties on a bound object, it will inspect the object’s type, find out the primitive CLR-type of a bound property, and will add default validation rules for each type which use XAML-configurable validation messages.

At the heart of the TypeValidationRuleSetter class is the following method:

Protected Overrides Sub AttachRules(ByVal base As Object)
    Dim f As FrameworkElement = TryCast(base, FrameworkElement)
    If f IsNot Nothing And ReferenceType IsNot Nothing Then
        If f IsNot Nothing Then
            Dim bg As BindingGroup = f.BindingGroup
            If bg IsNot Nothing Then
                For Each be As BindingExpression In bg.BindingExpressions.OfType(Of BindingExpression)()
                    Dim propertyName = be.ParentBinding.Path.Path
                    Dim propertyInfo As System.Reflection.PropertyInfo = ReferenceType.GetProperty(propertyName)
                    Select Case propertyInfo.PropertyType
                        Case GetType(DateTime)
                            be.ParentBinding.ValidationRules.Add(New DateValidationRule(Me.DateMessage))
                        Case GetType(Int16)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int16.MinValue, Int16.MaxValue, Me.IntegerMessage))
                        Case GetType(Int32)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int32.MinValue, Int32.MaxValue,Me.IntegerMessage))
                        Case GetType(Int64)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Int64.MinValue, Int64.MaxValue, Me.IntegerMessage))
                        Case GetType(Single)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Single.MinValue, Single.MaxValue,Me.NumericMessage))
                        Case GetType(Double)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Double.MinValue, Double.MaxValue, Me.NumericMessage))
                        Case GetType(Decimal)
                            be.ParentBinding.ValidationRules.Add(New NumericValidationRule(Decimal.MinValue, Decimal.MaxValue, Me.NumericMessage))
                        Case GetType(Integer)
                            be.ParentBinding.ValidationRules.Add(New IntegerValidationRule(Integer.MinValue, Integer.MaxValue, Me.IntegerMessage))
                    End Select
                    If Setters.Contains(propertyName) Then
                        For Each v As System.Windows.Controls.ValidationRule In Setters.Item(propertyName).ValidationRules
                            be.ParentBinding.ValidationRules.Add(v)
                        Next
                    End If
                Next
            End If
        End If
    End If
End Sub

 

This method will be called during the Initialized event, when Bindings have been established but still can be modified. It inspects the type of the property and adds one of a choice of default ValidationRules, which basically make a difference only between date, integer and other numerical values, providing different value ranges as feedback to the end user. So, the class translates from the technical, physical representation, which is determined by the CLR type of the property, to the logical limitations that type entails, which is what is relevant for the user.

So, although there are different checks for different allowed value ranges, there are only three configurable, i.e. localizable, validation messages:

Public Property DateMessage As String = "Must be a date value."

Public Property IntegerMessage As String = "Must be an integer between {1} and {2}."

Public Property NumericMessage As String = "Must be a number between {1} and {2}."

 

The default ValidationRules use basic .Net functions in order to determine whether .Net can convert the value:

Protected Overrides Function ValidateCore(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As Boolean
    Dim result As Boolean = IsNumeric(value)
    If result And MinValue.HasValue Then
        result = CDbl(value) >= MinValue
    End If
    If result And MaxValue.HasValue Then
        result = CDbl(value) <= MaxValue
    End If
    Return result
End Function

 

Finally, the allowed minimum and maximum values are formatted into the validation message, to give the users complete feedback on what they have to enter in order to make it a valid value:

Protected Overrides Function FormatMessage(ByVal value As Object) As String
    Return String.Format(Me.Message, value, MinValue, MaxValue)
End Function

 

Conclusion

The TypeValidationRuleSetter class allows it to include meaningful and localizable validation messages for type checks with minimal effort. At the same time, it protects the application from exceptions which would otherwise be thrown because of type mismatches between what is entered into a TextBox and the type of the bound property.

Localized Value Formatting in WPF

January 14, 2010 3 comments

When I introduced the WPFGlue localization components, I concentrated mainly on displaying localized strings and other static information in the user interface. Today I want to complete the WPFGlue localization section by discussing localized value formatting.

The Problem

Number and date formats vary from culture to culture. 30/12/2009, 12/30/2009 and 30.12.2009 all refer to the same day, and 12.345,67 and 12,345.67 are actually the same number, but will turn out very different if the computer doesn’t know which format the user assumed. Since the early days of computers, this problem has been solved by defining the culture under which the system runs as a system property. Then there would be a set of predefined formats for each given culture which implement the usual standards in that culture. Applications would take number and date formats from these system settings, so they could use the local standards even though they were developed in a different language.

A special case of this problem are enumerated lists. Usually, there is a short text that describes each option. In a localized application, this text should be translated according to the current culture, and it would also be necessary to sort the list differently in different languages in order to keep up an alphabetic order of the items.

Specifying the Formatting Culture in WPF

In WPF, value formatting is part of data binding: if a control needs to display a bound value, it converts it into a string and displays the string. The culture which is used can come from three sources:

  • Default: if you don’t do anything special to tell the system from which culture it should derive its number and date formats, it will just take standard US English. Makes sense to define one culture so that by default the application will look the same on whatever system it runs. But actually, in the rest of Dot.NET the rule is that the application should take the installed system culture as default for these purposes, so WPF breaks the expectations of especially us poor Non-Americans…
  • The Language property or xml:lang attribute: WPF FrameworkElements have a Language property, which can accept a IETF language tag like en-US or de-DE, and will set the date and number formats according to this language and culture’s properties. The definitions for the formats come from a set of preinstalled cultures which are part of the Dot.Net framework. The Language property is inherited, so if it is specified on top of a page, it will take effect on the whole page.
  • The ConverterCulture property on the Binding: This property accepts a Globalization.CultureInfo object. This object might come from any source. Especially, if the user changed the standard number and date formats on the system using the Language and Regional Settings in Control Panel, these changes will be visible in the CurrentCulture object of the application, while they won’t be visible if one just sets the Language property.

So, setting the ConverterCulture on the bindings would be the preferred way to respect the system’s and user’s formatting preferences. However, there are two drawbacks: first of all there is no built-in way to get at the system culture in XAML, and second one would have to specify the culture on each and every binding, since there is no way to apply this setting in a way that is global to the application.

In order to solve this problem, I added another MarkupExtension to the WPFGlue localization namespace: Binding. This is actually nothing more than a normal Binding with the ConverterCulture preset to the CurrentCulture of the system:

Namespace Localization
    Public Class Binding
        Inherits System.Windows.Data.Binding

        Public Sub New()
            MyBase.New()
            ConverterCulture = System.Globalization.CultureInfo.CurrentCulture
        End Sub
        Public Sub New(ByVal path As String)
            MyBase.New(path)
            ConverterCulture = System.Globalization.CultureInfo.CurrentCulture
        End Sub
    End Class
End Namespace

 

So, one can use it everywhere instead of the default Binding, and get date and number formats that behave according to the system settings. I’m not especially proud of this solution, but for the time being it seems to be the best compromise.

The LocalizationExample which you find in the WPFGluePublished solution on the Downloads page demonstrates this behaviour. This XAML code:

    Title="LocalizedPage" Language="{l:UILanguage}" FlowDirection="{l:FlowDirection}">
    <Page.Resources>
        <s:DateTime x:Key="Value">#12/30/2009#</s:DateTime>
    </Page.Resources>
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
            <TextBlock Text="{l:String TestString}" />
            <TextBlock Text="{Binding Source={StaticResource Value}}"/>
            <TextBlock Text="{l:Binding Source={StaticResource Value}}"/>
            <Image Stretch="None" Source="{l:BitmapImage FlagURI}"/>
        </StackPanel>
    </Grid>
</Page>

 

produces the following output:

image

 

Notice that setting the page’s Language property to the UILanguage (German in my case) causes the first binding to display its value in the default German date format, while using the localized binding causes the same value to be displayed in the format YYYY-MM-DD, which is what I specified in Control Panel as the standard short date format.

Localized Lists

In Dot.NET, sets of options can and should be summarized in enumeration types, with a named constant for each available option. The Dot.NET formatting engine, which is called through the String.Format method, supports converting the numeric enumeration values to strings and vice versa using the constant names that are defined for the enumeration in code. However, this option is only useful for debug output. If the values must be displayed to users, they must be translated into their languages, and actually this is not the job of the code which defines the enumeration.

A user interface which wants to use enumeration values needs the following functions:

  • translate numerical values to string descriptions (for read-only display of values).
  • translate string descriptions into numerical values (for cases where the user can enter text).
  • create a mapped list of values and descriptions (for ComboBoxes and the like where the user just picks a value from a list).

The LocalizedList class provides these functions. It works both as a collection of value / description pairs and as a IValueConverter that converts the descriptions forwards and backwards.

In order to define a LocalizedList, you have several options:

  • Just set the ValueEnum to an enumeration type and let the LocalizedList generate the entries from the enumeration’s definition (actually, this is not localizing anything yet…)
  • Set the LocalizedList’s Definition property to a string in the format “value1=name1,value2=name2…”, where the values are the constant names or values from the enum definition, and the names the localized display names. Typically, the definition string would itself be a localized string resource.
  • Define Entry objects directly in the XAML, which map string keys to arbitrary typed values.

The definition string format is actually quite flexible. So, you can change the default list and value separator characters (“,” and “=”). You could also leave out the values and just specify the enumeration type, so that the LocalizedList assumes that you specified the descriptions for the numeric values in their natural order. On the other hand, you could specify both names and values, but change the order, or use a subset of the available values, only. And finally, you could leave out the ValueEnum altogether and use String codes instead, as it is done in the language selection list in the example:

<l:LocalizedList x:Key="languageList" Definition="{l:String LanguageList}"/>

where e.g. the English version of the referenced string resource is

de-DE=German,en-US=English,ar-SA=Arabic

The other LocalizedList in the example maps String keys directly to objects:

<l:LocalizedList x:Key="assemblies">
    <l:Entry Key="{l:String InternalLabel}" Value="{x:Null}"/>
    <l:Entry Key="{l:String ExternalLabel}" Value="{x:Type r:Dummy}"/>
</l:LocalizedList>

 

Using this list, it is possible to change the resource assembly of the example purely in XAML, with no code behind:

<ComboBox x:Name="SelectAssembly" ItemsSource="{StaticResource assemblies}"
          DisplayMemberPath="Key" SelectedValuePath="Value"
          SelectedValue="{Binding RelativeSource={RelativeSource
          Mode=FindAncestor, AncestorType={x:Type NavigationWindow}},
          Path=(l:Localization.ResourceReferenceType)}"/>

 

Conclusion

Using localized resources on the one hand and providing localized value formatting on the other hand are about all that comes to my mind under the heading of localization. I’ll be glad to react to feedback or criticism…

Categories: Localization Tags: , ,

Using Validation.ErrorTemplate

December 16, 2009 8 comments

This was originally published as a thread in the MSDN WPF forum before I started this blog. However, today I revisited the thread and decided to maintain it here, now..

There are a couple of known problems and unknown features with Validation.ErrorTemplate that came up frequently, and I’d like to collect the workarounds for these things in one place. This is how far I got:

Problem: The Content of the ErrorTemplate is not displayed correctly

e.g. the red border doesn’t fit around the control, or parts of the error template are not drawn. The reason is that there may not be enough space around the control to display the error template, so the solution is to make the margin of the control big enough that the error template can be drawn.

Unknown Feature: The DataContext of the ErrorTemplate is the Validation.Errors Collection

Haven’t found this in the documentation, but it means that the easiest way to get at an error message in an error template is to use {Binding Path=/ErrorContent}.

Problem: Have to Change the Style of a Control in Order to Set the Error Message as ToolTip

It seems to be impossible to set the ToolTip of a control using Styles or Triggers in the error template. Usually this is worked around by modifying the Style of the control, as in the example from the WPF SDK. However, this has the disadvantage that one has to change the style of the control in order to accommodate Validation, which might interfere with other uses of the Style. There are better workarounds: most elegant by using a ToolTip on some error indicator that is part of the error template, or, if it must be the control itself, using a custom attached property on the AdornedElementPlaceholder like this:

Public Shared ReadOnly AdornedElementToolTipProperty As DependencyProperty = DependencyProperty.RegisterAttached("AdornedElementToolTip", GetType(Object), GetType(Validation), New PropertyMetadata(Nothing, AddressOf OnAdornedElementToolTipChanged))
Public Shared Function GetAdornedElementToolTip(ByVal d As DependencyObject) As Object
    Return d.GetValue(AdornedElementToolTipProperty)
End Function
Public Shared Sub SetAdornedElementToolTip(ByVal d As DependencyObject, ByVal value As Object)
    d.SetValue(AdornedElementToolTipProperty, value)
End Sub
Private Shared Sub OnAdornedElementToolTipChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    Dim placeholder As AdornedElementPlaceholder = TryCast(d, AdornedElementPlaceholder)
    If placeholder IsNot Nothing Then
        Dim control As Control = TryCast(placeholder.AdornedElement, Control)
        If control IsNot Nothing Then
            control.ToolTip = e.NewValue
        End If
    End If
End Sub

One can bind the error message to this property, and this way the error template works without interfering with the control’s style.

Problem: The Error Template is not Displayed when a Page is Loaded

This is by design, since one could assume that the user doesn’t want to see error messages before he/she made any mistakes, but sometimes one needs this functionality. So, the ValidatesOnTargetUpdated property was introduced on the ValidationRule class; by setting it to true, one sees the validation result immediately.

However, there is one caveat: you must make sure that you set the DataContext after the page is initialized; this would be either in the constructor after the generated comment line that says that initialization code should go there, or in the Loaded event. If you want to set the DataContext in XAML, you find a solution for this problem here (among other things): https://wpfglue.wordpress.com/2009/12/08/navigating-from-object-to-object/

Anyway, I hear this will be fixed in WPF 4.0.

Problem: When Using an Expander, the Error Template Remains Visible even if the Expander is Collapsed

This problem is a known bug, unfortunately as far as I know not yet scheduled for fixing. The workaround (found in the MSDN WPF forum) is binding the Visibility property of the outermost element of the error template to the AdornedElement.IsVisible property of the AdornedElementPlaceholder, using the BooleanToVisibilityConverter that comes with WPF.

Standard Error Template

So, this would be my standard error template:

<ControlTemplate x:Key="errorTemplate">
    <Border BorderBrush="Red" BorderThickness="1">
        <Border.Visibility>
            <Binding ElementName="placeholder" Path="AdornedElement.IsVisible">
                <Binding.Converter>
                    <BooleanToVisibilityConverter/>
                </Binding.Converter>
            </Binding>
        </Border.Visibility>
        <StackPanel>
            <AdornedElementPlaceholder x:Name="placeholder"
             v:Validation.AdornedElementToolTip="{Binding Path=/ErrorContent}"/>
        </StackPanel>
    </Border>
</ControlTemplate>

And this would be how I use it:

<StackPanel>
    <TextBox x:Name="Abox" Margin="3" Validation.ErrorTemplate="{StaticResource errorTemplate}"
      Text="{Binding Path=A, UpdateSourceTrigger=PropertyChanged,
      ValidatesOnDataErrors=True}" />
    <Expander Header="Test" IsExpanded="True" Validation.ErrorTemplate="{x:Null}">
        <Expander.BindingGroup>
            <BindingGroup/>
        </Expander.BindingGroup>
        <TextBox x:Name="Bbox" Margin="3"
         Validation.ErrorTemplate="{StaticResource errorTemplate}"
         Text="{Binding Path=B, UpdateSourceTrigger=PropertyChanged,
          ValidatesOnDataErrors=True}"/>
    </Expander>
</StackPanel>

Categories: Validation Tags: , ,

Using .Net Localized String Resources in WPF

December 5, 2009 1 comment

When localizing an application, the most obvious feature is that labels, texts and pictures in the UI are displayed in another language. This is done through localized string resources, and .Net has a whole framework for handling these. In Windows Forms, this framework is quite well supported by Visual Studio. However, in WPF this support is considerably lacking. This is about how you can use them anyway.

Creating Localized Resources

You can edit the localized resources of your application if you choose the Resources tab in the My Project editor. However, these are the resources for the neutral or invariant culture, the .Net culture description that is not connected to any natural language. These resources are embedded into the executable of your application. If you want culture specific resources which are compiled into .Net satellite DLLs which you can distribute with your application in order to let it support multiple languages, you have to create them manually. Unfortunately, the documentation on this is not very clear. In my tests, I did the following:

  • Close Visual Studio
  • Go to the My Project / MyExtensions folder in the project directory
  • Copy the file Resources.resx and rename the copy to the correct name for the culture, e.g. Resources.de-DE.resx for German or Resources.en-US.resx for US English
  • Open the Project again in Visual Studio
  • Select Add Existing Item and add the file to the project.
    After you have done that, the Resource designer will display if you open the file from Solution Explorer. Just make sure that the Access Modifier combo box on top says “No Code Generation”, otherwise you might get error messages about conflicting class names on compilation.

Since you copied the original resource file, all resources will already be there in the original language; so, if you do this towards the end of the project, you will already have the correct resource keys and their original translations to help you…

The Visual Studio Resource designer was originally intended for Windows Forms. As a result, the image resources it produces (at least at the time of writing, Visual Studio 2008 SP1) are tied to the Windows Forms image types, which are not working with WPF. For this reason, I decided to not support image resources. Instead, one can create bitmap images from URIs which are stored as resource strings, so that they can point to different sources for different languages. These URIs should be pack URIs if you provide the images with your program; pack URIs are explained very nicely here.

Using Localized Resources in Your WPF Pages

WPFGlue offers four different MarkupExtensions for integrating localized resources in XAML. Their names are StringExtension, URIExtension, BitmapImageExtension, and FlowDirectionExtension. StringExtension is the base class. Its ProvideValue method, which is called whenever the XAML that contains the extension is instantiated, looks like this:

Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
    Dim result As Object = "[" & Key & "]"
    If _ResourceManager Is Nothing Then
        Try
            If Assembly IsNot Nothing AndAlso Not Assembly.FullName.StartsWith("PresentationFramework,") Then
                _ResourceManager = New System.Resources.ResourceManager(BaseName, Assembly)
            End If
        Catch x As Exception
            result = Assembly.FullName
        Finally
        End Try
    End If
    If _ResourceManager IsNot Nothing Then
        Try
            result = _ResourceManager.GetString(Key)
        Finally
        End Try
        If result Is Nothing Then
            result = "[" & Key & "]"
        End If
    End If
    Return result

End Function

It reads strings from the application’s resources; URIExtension converts them into URIs on top of that, and BitmapImageExtension creates a BitmapImage from the URI it looked up in the resource. FlowDirectionExtension checks whether the current UI language is written from right to left, and returns the correct FlowDirection value. All these markup extensions behave like StaticResource. This means that they return their value at the time the XAML is instantiated, and don’t create a binding to the properties they are applied to. It also means that you can use them everywhere in your XAML, not only with DependencyProperties.

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
    <TextBlock Text="{l:String TestString}" />
    <TextBlock Text="{Binding Source={StaticResource Value}}"/>
    <TextBlock Text="{l:Binding Source={StaticResource Value}}"/>
    <Image Stretch="None" Source="{l:BitmapImage FlagURI}"/>
</StackPanel>

So far so good. Using these extensions, you make sure that the users see the application in their system’s language, if you provided the correct resources. But what if you want to change the language at runtime?

This is where it gets sticky. The UILanguage Sticky Property is used to read and set the current UI language, which is the same language the resource manager uses in order to find the resources it wants to display. However, changing this property will not automatically change all the labels. I didn’t think this necessary; in this solution, changes of the UI language will take effect only after you navigate to a different page.

Public Shared ReadOnly UILanguageProperty As DependencyProperty = DependencyProperty.RegisterAttached("UILanguage", GetType(String), GetType(Localization), New PropertyMetadata(Nothing, AddressOf OnUILanguageChanged, AddressOf CoerceUILanguage))
Public Shared Function GetUILanguage(ByVal d As DependencyObject) As String
    Return d.GetValue(UILanguageProperty)
End Function
Public Shared Sub SetUILanguage(ByVal d As DependencyObject, ByVal value As String)
    d.SetValue(UILanguageProperty, value)
End Sub
Private Shared Sub OnUILanguageChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    If e.NewValue IsNot Nothing Then
        System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(CStr(e.NewValue))
    End If
End Sub
Private Shared Function CoerceUILanguage(ByVal d As DependencyObject, ByVal baseValue As Object) As Object
    Dim result As String = baseValue
    If baseValue Is Nothing Then
        result = System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag
    End If
    Return result
End Function

Notice that setting this property to {x:Null} explicitly will coerce its value to the current UI language. You can use this to initialize a language selection box to the correct start value.

Setting UILanguage on any element in XAML will change the UI language for the whole application. However, if you want a common and consistent access point for this, consider setting it on the topmost XAML element in your application, i.e. the main Window or NavigationWindow.

Using Resources from Different Assemblies

You can use l:String etc. just out of the box, without having to tell the system where it would find the resources. This is possible because the MarkupExtension uses the type of the current Application object in order to find the assembly of the main program.

In the designer, obviously the main program is Visual Studio, so the markup extension doesn’t know where to look. If it isn’t able to discover the ResourceManager it needs to provide the string it is looking for, or if the key it receives does not have a corresponding resource, it just returns the value of the key itself enclosed in brackets.

However, there is a way to tell it where to look. This has to be there in order to be able to read resources from assemblies other than the main program’s assembly. For example, you could have put all your resources into a special resource assembly, or you could use a library which brings along its own resources.

The most convenient way to make a different assembly the resource lookup source is to assign a type from that assembly to the ResourceReferenceType sticky property. This assumes that the basename for resources in this type’s assembly is “Resources”, as is the default with VB.Net. You can also have finer control setting the ResourceAssembly and ResourceBasename properties, respectively. (n.b. I’m not sure whether the automatic detection of the assembly and basename works the same way in VB and C#).

These settings influence the whole application. This means that as long as they point to a certain place, all resource strings in the application are taken from there. That looks like quite a serious limitation; however, if using a Navigation application, every page would be independent and could easily have its own setting, and if you’d divide your page into regions using containing elements, the behaviour would be consistent as long as every containing element and every data template sets the source for its localized resources explicitly. This is so because the markup extensions access the resource DLL only while the XAML is instantiated, and the set properties retain their values even if the resource assembly changes later on (*).

If you want to preview your resource strings in the designer, you can just set the ResourceReferenceType and UILanguage on the window you are working on to your program’s application class and language, respectively.

<Page x:Class="LanguageSelectionPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:l="https://wpfglue.wordpress.com/localization&quot;
    xmlns:ex="clr-namespace:LocalizedResourcesExample"
    xmlns:r="clr-namespace:LocalizedResourceAssembly;assembly=LocalizedResourceAssembly"
    Title="LanguageSelectionPage" FlowDirection="{l:FlowDirection}"
      l:Localization.ResourceReferenceType="{x:Type ex:Application}"
      l:Localization.UILanguage="de-DE">

     However, if you want to be able to change the resource assembly and language at runtime, don’t forget to remove these attributes before you build the application.

The LocalizedResourceExample

The example solution you can download from the Downloads page demonstrates how to select the UI language at runtime:

<ComboBox SelectedValue="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
    AncestorType={x:Type NavigationWindow}}, Path=(l:Localization.UILanguage)}"
    ItemsSource="{StaticResource languageList}" DisplayMemberPath="Key"
    SelectedValuePath="Value"/>
</StackPanel>

 

Notice that the ComboBox sets the value of the UILanguage attached property directly on the NavigationWindow that hosts the page. However, you have to navigate away from the page in order to see the changes applied. If you go back to the language selection page, you will see that this page also re-reads its resources and displays differently dependent on what is selected in the language selection box.

The second combobox will select the assembly LocalizedResourceAssembly in order to access its resources. Apart from the resources themselves, this assembly contains only an empty class called “Dummy” which serves as reference class to find the resources. Since the LocalizedResourceAssembly doesn’t contain all the resources defined in the main assembly, you can watch the fallback behaviour for the resource lookup: If the resource isn’t there in the specified language, it is taken from the main culture of the application (which is de-DE German in the example), and if it is not found there, the key of the resource will be displayed. Since this fallback mechanism only works within the same resource assembly, you will see different fallback behaviour if you choose the external resources.

Look out for..

The methods described here help in localizing the descriptive elements of your user interface, and given the resources are already there, everything is controlled completely through XAML. In order to localize value formats, you need some other techniques. They are already contained in the example, but I will explain them in a later post.

Credits

I learned how to write a MarkupExtension from this article by Christian Moser. Even though I changed his approach considerably, I found it extremely helpful, and especially if you want to take changes in the UI Language to take effect immediately, you should look at his example.

 

 

(*) Footnote: In order to make this fool proof, one could make the ResourceReferenceType property inheritable, so that it is copied to all child elements. However, I try to avoid FrameworkPropertyMetadataOptions.Inherit if possible, even if I don’t think that looking around for the correct ResourceManager on every localized string is very expensive in terms of performance.

Localization in WPF

December 5, 2009 Leave a comment

There are lots of good articles on localizing WPF applications out there, so here it is more a matter of commenting and choosing different approaches than of creating new value. But before I start, I’d like to discuss the jobs of localization a bit, and explain my choices in how to handle them.

Providing a Localized User Interface

The most obvious effect of localization is to provide labels, descriptions and pictures which contain texts in the current user’s language. So, the two bits of information one needs in order to provide this are the user’s language and the source of the texts. In .Net, localized resources are contained in resource DLLs, which are provided together with the main program executable, and are accessed through the ResourceManager class internally, already taking into account the language of the current thread.

Visual Studio supports two ways of editing localized resources: the one is directly through the My Project / Resources editor, the second is indirectly as part of the Windows Forms designer. Unfortunately, there is no such support for WPF. There is a command line tool named LocBAML which is supposed to extract all localizable strings from compiled XAML and to allow to create the localized resource DLLs from it, but this is not very integrated into the development process and requires changes to the XAML itself using UI ids (which are a good thing to have but a nightmare to maintain). So, most examples I found ignore this approach and instead make for a way to make the usual localized resources from the Resource editor available to WPF.

The obvious solution is a MarkupExtension that accepts the name of the resource as a parameter and returns its value. Some of the published components allow to change the language at runtime; I didn’t think this necessary, so my implementation just sets the value, which means you have to close and reopen forms in order to make a language change visible. However, it has the neat little feature of being able to find the ResourceManager of the main program’s assembly by itself, so you don’t have to configure anything in order to be able to use it. Find it explained here.

Formatting and Parsing Values According to Localized Formats

Formatting and parsing values that are displayed to or edited by the user needs to take into account local variations in formatting conventions. 12.456,70 and 12,456.70 are actually the same number, but if you’d ask an American and a German they’d probably disagree on what number that is.

Recognizing these formats has been a job for localization components ever since computers have been invented, so it’s quite well supported in .Net. However, for reasons unknown WPF’s default language for all these purposes is US English, regardless of the system’s settings, let alone the culture of the current thread. In order to change the language for formatting, you have to set the Language attribute. There are some ways to do it; one way one frequently sees is to override its default value to be the system’s culture on application startup, but I find this a little bit harsh, and I think you can’t undo it if you want to change the language at runtime. So I prefer to set the Language attribute on top of the page or window, so that it is inherited by all the controls. In order to get at the language more conveniently, I implemented MarkupExtensions that return the current language or the current UI language, respectively.

There are cases where this behaviour isn’t enough. As I learned recently, even setting the Language property to the CurrentCulture doesn’t make WPF acknowledge changes in date and number formats users might have made in the Regional Settings of their computer. In order to work around this, one would have to use a custom Binding extension, which might be a good idea anyway, since it provides a way to hook all sorts of useful little helpers to bindings without changing the XAML code too much. But actually, I don’t like this solution and hope there will be a fix soon…

A special case of localized formatting is providing translations for Enum values. .Net can parse Enum values from their string equivalents, but unfortunately those string equivalents are hardcoded with the definition of the Enum. Therefore, WPFGlue contains a LocalizedList, which fills two jobs: It works as a Converter, converting the Enum values into localized strings and vice versa, and it can be used as ItemsSource for a ComboBox or other Selector in order to let the user pick from the available values. Its intended way of usage is defining an instance of it in a ResourceDictionary, using an initialization string which maps the invariant string representations to localized counterparts. As an extra, it can produce and parse strings that are combinations of multiple Enum values.

You can find descriptions and examples for these components here.

Switching the Keyboard to Different Languages

Handling different languages with different keyboard layouts within the same document has only recently become a common feature in text processing applications. However, since I frequently have to work with data in different languages, I wouldn’t like to miss this feature.

In WPF, the class responsible for managing different keyboard languages is called InputLanguageManager. It exposes an attached property, InputLanguage, that accepts a culture identifier and switches the keyboard to the defined layout for this language, if one is installed. Unfortunately, this only works when the control receives focus. So, if you are typing something in a TextBox and change the InputLanguage while you are typing, e.g. through a keyboard shortcut, this change doesn’t become active unless the keyboard focus leaves the TextBox and returns again.

Also, in principle you can define different languages for paragraphs in a RichTextBox by setting the InputLanguageManager.InputLanguage on the Paragraph elements in the RichTextBoxes FlowDocument, but the keyboard doesn’t automatically change to the appropriate layout when the selection enters a section in a different language. This feature is extremely useful, so I decided to create a Sticky Property which allows to change the keyboard language effective immediately, and a Sticky Behaviour that sets the current input language according to what is set in this attached property on the FrameworkContentElements inside a RichTextBox’s FlowDocument. This property is explained here. Recently I found a post on Connect which suggests that there might be a native solution for this problem in WPF 4.

Links

Christian Moser describes a good example for localization by MarkupExtension here.

Categories: Localization Tags: ,

CSS Class Equivalent in WPF

November 30, 2009 1 comment

In the small hours of this morning, I had an idea about how to create a stylesheet for FlowDocuments that behaves similar to CSS. What’s the difference between CSS classes and WPF styles? Well, CSS classes are not necessarily specific to a certain element type, and they can be combined on the styled element, while WPF Styles are specific to the element type, and each element can have exactly one Style (in addition to the default style, which fills in wherever the element style doesn’t provide a value). Also, if you want to combine Styles in WPF, you have to do it in the Style definition using BasedOn, which I find quite inflexible, and which drives one crazy if one has to support a lot of independent permutations.

So, what was the idea? The first one was that CSS classes should not be implemented as Styles, but as Triggers inside styles, which are selected whenever a certain attached property has the fitting value. So, in order to support different classes of TextBlocks, one would do this:

<Style TargetType="TextBlock" >
    <Style.Triggers>
        <Trigger Property="t:MultiStyle.Selector" Value="italic">
            <Setter Property="FontStyle" Value="Italic"/>
        </Trigger>
        <Trigger Property="t:MultiStyle.Selector" Value="red">
            <Setter Property="Foreground" Value="Red"/>
        </Trigger>
        <Trigger Property="t:MultiStyle.Selector" Value="bold">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="FontStyle" Value="Normal"/>
        </Trigger>
    </Style.Triggers>
</Style>

 

The second idea was to use a special type for the attached property. The StyleSelector class tokenizes its String value, and returns “equal” on a comparison with another string or StyleSelector if the other object contains one of the tokens that are defined:

Public Class MultiStyle
    Inherits DependencyObject

    Public Shared ReadOnly SelectorProperty As DependencyProperty = DependencyProperty.RegisterAttached("Selector", GetType(StyleSelector), GetType(MultiStyle))
    Public Shared Function GetSelector(ByVal d As DependencyObject) As StyleSelector
        Return d.GetValue(SelectorProperty)
    End Function
    Public Shared Sub SetSelector(ByVal d As DependencyObject, ByVal value As StyleSelector)
        d.SetValue(SelectorProperty, value)
    End Sub

End Class

<System.ComponentModel.TypeConverter(GetType(StyleSelectorConverter))> _
Public Class StyleSelector
    Implements IEquatable(Of StyleSelector), IEquatable(Of String)

    Public Sub New(ByVal value As String)
        _Value = value
    End Sub

    Private _Value As String
    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(ByVal value As String)
            _Value = value
        End Set
    End Property

    Public Overrides Function Equals(ByVal obj As Object) As Boolean
        If TypeOf obj Is StyleSelector Then
            Return Equals1(obj)
        End If
        If TypeOf obj Is String Then
            Return Equals2(obj)
        End If
        Return MyBase.Equals(obj)
    End Function

    Public Function Equals1(ByVal other As StyleSelector) As Boolean Implements System.IEquatable(Of StyleSelector).Equals
        If other IsNot Nothing And _Value IsNot Nothing Then
            Dim myValue() As String = _Value.Split(" "c)
            Dim otherValue() As String = other.Value.Split(" "c)
            Dim result As Boolean = False
            Dim i As Integer = 0
            Do While i < otherValue.Length And Not result
                result = myValue.Contains(otherValue(i))
                i += 1
            Loop
            Return result
        Else
            Return MyBase.Equals(other)
        End If
    End Function

    Public Function Equals2(ByVal other As String) As Boolean Implements System.IEquatable(Of String).Equals
        If other IsNot Nothing And _Value IsNot Nothing Then
            Dim myValue() As String = _Value.Split(" "c)
            Return myValue.Contains(other)
        Else
            Return MyBase.Equals(other)
        End If
    End Function
End Class

Public Class StyleSelectorConverter
    Inherits System.ComponentModel.TypeConverter

    Public Overrides Function CanConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal sourceType As System.Type) As Boolean
        Return (sourceType Is GetType(String))
    End Function

    Public Overrides Function CanConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As System.Type) As Boolean
        Return (destinationType Is GetType(String))
    End Function

    Public Overrides Function ConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object) As Object
        Dim result As StyleSelector = Nothing
        If TypeOf value Is String Then
            result = New StyleSelector(value)
        End If
        Return result
    End Function

    Public Overrides Function ConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As System.Type) As Object
        Dim result As Object = Nothing
        If TypeOf value Is StyleSelector Then
            result = DirectCast(value, StyleSelector).Value
        End If
        Return result
    End Function
End Class

The only thing that remains to do is to implement the TypeConverter that allows to use StyleSelector as an attribute value in XAML, and to define the attached property.

Applications

Since I couldn’t go back to sleep, I tried it out immediately. You can download the VB solution here. (you will have to remove the .doc extension from the zip file; wordpress.com doesn’t allow zip files directly.)

However, when I was done, the idea didn’t seem so brilliant anymore as when I woke up. There are limitations of course. The trick works only with property setters. If you want to use triggers, they will be the same for all classes. What can you do with this what you can’t do with styles? The last thing that I came up with is that one could write a style sheet editor that creates styles with identical class definitions for different types of controls, so that you can use the same set of property values for different elements, as in CSS. However, you’d have to have this editor, otherwise it would be to much work to keep the class definitions equal to each other. And, of course, one can combine multiple classes on the same element, for what it’s worth…

PS: What happens if two classes provide different values for the same property and are both set on the same element? Because triggers are executed in the order they appear in the Style definition, the class that is defined last wins and overwrites the value of its sibling…

Categories: Uncategorized Tags: , , ,