Label 'Texts' and custom combo box values Implementation

Topics: Prism v2 - WPF 3.5
Apr 20, 2009 at 3:49 AM
Hi,

I am new to this library and to WPF in general and would like to ask for your thoughts.

I have started a new WPF project, which is a redevelopment of a previous working project, and decided to use this guidance library.  But am still not clear on how to implement one of my requirements.

My requirement is to have a service registered into our shell and can be used by the modules that we create.  We call this service TermsService.  Basically, since we are creating a project for different countries, we decided to put our 'translatables' (I don't know if this is even a word =) ) into the database.  'Translatables' are used for labels, common combo box values, i.e. Gender, etc.  We did it this way, so instead of creating resources for every culture, we just change the database.  That way, we don't have to recompile anything and the application is mostly controlled by the database.  And this is what the TermsService is for: to provide functions/methods to retrieve the label value from the db by passing a 'termId'.  This is how the 'previous' application (WinForms) was developed which used SCSF.

In the previous version of our application, what it does is on a 'Language' change event, i.e. in runtime, it was changed from English to Chinese, we pass the call method accepting the form/view as parameter.  This method then loops through all the controls inside the form/view and uses the 'termId' stored in the tag of each label control to set the 'Text' property.  But I don't see this as an implementation in WPF.  Hence my confusion.  As much as possible I want to stick to the Model-View-ViewModel pattern which I have already done to most of the modules I created.

So there are two things that I wanted to do which I really can't figure out:

1. I need to recreate a custom combo box which is windows combo box with an additional property  to hold the termId:
    I.e in the previous version we just use:  
        CustomComboBox ccb = new CustomComboBox();
        ccb.TermId = "GENDERTERMID";
    this now populated the combobox with appropriate values.  This control uses the TermsService I was referring to earlier to retrieve the values.

2. And second, and most important, is to be able to change the labels depending on the language selected by the user.   I wanted to use in xaml something like:
    <Label Tag="NAMETERMID" />
    And when view is loaded, this label will have a value of "Name: "
    
    In the previous version, the Tag property if the label is just set to "NAMETERMID".  And it shows with the correct text, ie: "Name: ".
    
I've tried a lot of things already, and am at a lost.  I tried binding to methods, etc but can't seems to find the correct implementation.  Also I'm not able to 'inject' the term service into the view (usercontrol), or I shouldn't do that?  I can retrieve the termsService registered in the ViewModels but not in the views.
    
What am I doing wrong? Do you have any ideas on how I'll be able to implement this?  Any help would be appreciated. Or please just point me to the right direction.

Thanks very much.

James

Apr 20, 2009 at 2:47 PM
Edited Apr 20, 2009 at 2:47 PM
James,

Since it seems like all views across your modules will need the capability to localize text based on a selected language this is the approach I would take:

  • Define a "LocaleChanged" event in your Infrastructure project.
  • Define the TermsService in your Infrastructure project.
  • Upon start-up, register the TermsService with the UnityContainer using the ContainerControlledLifetimeManager option.
  • In the constructor of each of your ViewModels subscribe to the LocaleChanged event.
  • Expose a method on your TermsService which each ViewModel can call to get back an updated list of "terms" it needs.
  • Expose a property for each of the terms on your ViewModel (or you could create a view specific class which has all the terms exposed as properties and then expose that class as a property of your ViewModel).
  • Bind the text, content, etc to the property exposed on your ViewModel or sub-class.

The flow would then work like this:

ComboBox SelectedItemChanged -> Publish LocaleChanged event -> ViewModels would call the TermsService -> Properties updated on the ViewModel -> View pick up changes automatically.

One thing to note. You'll need to implement the INotifyPropertyChanged interface on all your ViewModels and any sub-classes which will have a View element bound to it.

Anyway, that's the approach I would take. There are probably holes in it, but at least it should help you get started. Let me know if you have any questions.

Ryan

Apr 21, 2009 at 12:58 AM
Hi Ryan,

Thanks for your reply.

Please bear with me, but I do have questions.  I have looked at what you suggested and already implemented the first steps.  But I do have a few clarifications I need to ask you about this step that you said:
  • Expose a property for each of the terms on your ViewModel (or you could create a view specific class which has all the terms exposed as properties and then expose that class as a property of your ViewModel).
With regards to this, what you're suggesting is that I need I need to create a property for each term/label?  So, it means, in one of the modules I got, is to create 100+ properties?  Because, this view is a product configuration module and you can set various settings in here.  It contains a lot of labels and what I understood is I can bind each label to the appropriate property in the model.  But this is going to be tedious? 

Is there any other way?  Do you think I need to create a custom label control with a TermService dependency property?  Is that the right way to go?
i.e <CustomLabel TermId="NAMETERMID" TermService={Bind ... }

I just don't know if this is an efficient way to go or does it adhere to WPF.  And to be honest, I don't know what to put in the Bind yet.  Or do I just Bind the 'Content' property.

Thanks again.  Appreciate any help.

Regards,

James

Apr 21, 2009 at 1:48 PM
Edited Apr 21, 2009 at 3:20 PM
James,

Sounds like you're heading down the correct path. Having said that, yes, it would be extremely tedious and inefficient to create 100+ properties for all the TextBlocks, Buttons, etc you'd need to localize. Luckily, I'm currently involved in a project which has a similar localization requirement and therefore I was able to take a couple minutes to create the sample below. A word of caution before you continue. The sample below doesn't utilized Prism - it's just a quick and dirty prototype I threw together using the out of the box SilverlightApplication template.

Now that we have that out of the way, here's what I did.

Added 2 classes to the SilverlightApplication project (see code below):

  • SampleViewModel.cs
  • LocalizedValueConverter.cs
SampleViewModel.cs

using

 

System.Collections.Generic;

 

 

using

 

System.ComponentModel;

 

 

namespace

 

LocalizedSilverlight

 

{

 

 

public class SampleViewModel : INotifyPropertyChanged

 

 

{

 

private Dictionary<string, string> _localizedText;

 

 

 

public Dictionary<string, string> LocalizedText

 

{

 

 

get { return _localizedText; }

 

 

 

set

 

 

{

 

if (_localizedText != value)

 

{

_localizedText =

 

value;

 

RaisePropertyChanged(

 

"LocalizedText");

 

}

}

}

 

 

public event PropertyChangedEventHandler PropertyChanged;

 

 

 

public SampleViewModel()

 

{

_localizedText =

 

new Dictionary<string, string>();

 

 

 

this.LocalizedText.Add("key1", "value1");

 

 

 

this.LocalizedText.Add("key2", "value2");

 

 

 

this.LocalizedText.Add("key3", "value3");

 

 

 

this.LocalizedText.Add("key4", "value4");

 

}

 

 

private void RaisePropertyChanged(string propertyName)

 

{

 

 

if (PropertyChanged != null)

 

{

PropertyChanged(

 

this, new PropertyChangedEventArgs(propertyName));

 

}

}

}

}

 


LocalizedValueConverter.cs

using

 

System;

 

 

using

 

System.Collections.Generic;

 

 

using

 

System.Windows.Data;

 

 

namespace

 

LocalizedSilverlight

 

{

 

 

public class LocalizedValueConverter : IValueConverter

 

 

{

 

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

 

{

 

 

return ((Dictionary<string, string>)value)[(string)parameter];

 

}

 

 

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

 

{

 

 

throw new NotImplementedException();

 

}

}

}

 


Next I modified Page.xaml to look like this:

<

 

UserControl x:Class="LocalizedSilverlight.Page"

 

 

 

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

 

 

 

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

 

 

xmlns

 

:local="clr-namespace:LocalizedSilverlight"

 

 

 

Width="400" Height="300">

 

 

 

 

<UserControl.Resources>

 

 

 

 

<local:LocalizedValueConverter x:Key="ConvertLocalizedValue" />

 

 

 

 

</UserControl.Resources>

 

 

 

 

<Grid x:Name="LayoutRoot" Background="White">

 

 

 

 

<StackPanel>

 

 

 

 

<TextBlock Text="{Binding Path=LocalizedText, Converter={StaticResource ConvertLocalizedValue}, ConverterParameter='key1'}" />

 

 

 

 

<TextBlock Text="{Binding Path=LocalizedText, Converter={StaticResource ConvertLocalizedValue}, ConverterParameter='key2'}" />

 

 

 

 

<TextBlock Text="{Binding Path=LocalizedText, Converter={StaticResource ConvertLocalizedValue}, ConverterParameter='key3'}" />

 

 

 

 

<Button Content="{Binding Path=LocalizedText, Converter={StaticResource ConvertLocalizedValue}, ConverterParameter='key4'}" />

 

 

 

 

</StackPanel>

 

 

 

 

</Grid>

 

</

 

 

UserControl>

 

 

And modified Page.xaml.cs like so:

using

 

System.Windows.Controls;

 

 

namespace

 

LocalizedSilverlight

 

{

 

 

public partial class Page : UserControl

 

 

{

 

public Page()

 

{

InitializeComponent();

 

 

SampleViewModel viewModel = new SampleViewModel();

 

 

 

this.DataContext = viewModel;

 

}

}

}

 

When you get all this running, you should see 3 labels with text of: value1, value2, value3 and a Button with content of: value4. As you can see, creating a value converter using the IValueConverter interface gives us a lot of flexibility in what we can do, is extremely easy, and prevented us from having to create a custom control in this scenario.

Hopefully this helps you out. Let me know if you have any other questions.

Ryan


Apr 22, 2009 at 2:37 AM
Hi Ryan,

Thanks so much for your help, I was able to make it run and it works great.
Just one final question, though.  More of a binding question

This works fine (same as what you have done):

<Label HorizontalAlignment="Right" Content="{Binding Path=LocalizedText, Converter={StaticResource ConvertLocalizedValue}, ConverterParameter='SYS1'}"/>

And then when I did this one, it also works:

                    <Label HorizontalAlignment="Right" Tag="SYS1">
                       <Label.Content>
                            <MultiBinding Converter="{StaticResource ConvertLocalizedValue}" >
                                <Binding RelativeSource="{RelativeSource Self}" Path="Tag" />
                                <Binding Path="LocalizedText" />
                            </MultiBinding>
                        </Label.Content>
                    </Label>

But when I place this (above) on the DockPanel.Resources like this (below), it doesn't work:

             <Style TargetType="{x:Type Label}"  >
                    <Setter Property="ContentTemplate">
                        <Setter.Value>
                            <DataTemplate DataType="{x:Type Label}">
                                <StackPanel>
                                    <TextBlock HorizontalAlignment="Right">
                                        <TextBlock.Text>
                                            <MultiBinding Converter="{StaticResource ConvertLocalizedValue}" >
                                                <Binding Source="LocalTerms" />
                                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Label}" Path="Tag"/>                                                    
                                            </MultiBinding>
                                        </TextBlock.Text>
                                    </TextBlock>
                                </StackPanel>
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>

I always get DependencyObject.UnsetValue on the first binding.  I was just thinking if I do it this way, I don't have to modify all my labels (inserting the converter, etc) and just leave it as it is.  Do you have any ideas why is this so?  Am I missing something?

Thanks so much.

Regards,

James

Apr 22, 2009 at 1:19 PM
James,

Unfortunately I've never worked with WPF before so the concept of MultiBinding is foreign to me and therefore I'm afraid I'll be of no use at this point. :-)

That does appear to be a very elegant solution if you can get it working. Please let me know if you discover the answer.

Good luck!

Ryan