Monday, April 28, 2008

WPF Localization - LocBaml

In my last post I talked about using resource files (resx) to localize your WPF application. This time it is about using LocBaml to localize your WPF application. The big difference in the two approaches is that LocBaml allows you to localize your application after the fact. That is, most applications you have to plan up front to do localization, but with LocBaml you can still localize your application after development. While lots of people will say this I don't really believe it. The reason this is not true in my opinion is because LocBaml only works if all your strings are sourced in XAML. This means that if you use strings that are located in a constant file or a resx file LocBaml  will not work to localize your application. Unless you are building a smaller application or you just happen to set up your architecture so all strings are sourced in XAML you are out of luck. So really when they say you can localize your application after the fact you really can only do it if you happened to conform your application to the requirements of LocBaml. On that note lets get into preparing the application with UIDs.

In my previous example I had three labels in my application. The first label had its text defined in XAML. The other two used resx files to define their text. In this walk through we will update the first label so we can localize it as well. The first thing we want to take note of is the current XAML layout of our label.

<Label Height="28" Margin="10,13,33,0" 
  Name="lblFromXAML" Content="Text from XAML" 
  VerticalAlignment="Top"></Label>
You will notice nothing special about the XAML here. Now we will run the LocBaml commands. To do this we open a visual studio command prompt and run msbuild with the updateuid command switch (msbuild /t:updateuid wpflocalization.csproj). After running this command we get the follow results in the command windows.

image

Now if you look at the XAML that makes up the label you will see the addition of UID attribute.

<Label x:Uid="lblFromXAML" Height="28" Margin="10,13,33,0" 
Name="lblFromXAML" Content="Text from XAML" 
 VerticalAlignment="Top"></Label>
This UID attribute tags each localization part of the application and will allow the LocBaml utility to find each one of these areas for localization. Now when you build the application you will have a .resource.dll file in the en-US folder. It is now time to jump through hoop 1 (really hoop two because setting up UIDs should be hoop 1). You must get the SDK and build the LocBaml project to get the LocBaml.exe. Once you have done this it is time for hoop 2. Run LocBaml against the US resource dll using the following syntax: LocBaml.exe /parse en-US/WpfLocalization.resources.dll /out:UsText.csv. This will export all the XAML fields marked with a UID to the csv file. The csv file that is created has  a few lines per UID item. Here is what the csv file looks like. Had enough yet? Sorry we are not done. You should notice that in the csv file only has the text for one of our three labels (lblFromXAML). The text that is the resource file is not pulled out into the csv file. This is because LocBaml only works when all strings are sourced in XAML (something you have to take into account at design time). Now you need to pass this csv file off to someone to translate. Now Hoop 3, it is time to run LocBaml again to create our de-DE resources.dll. Once that is done you can drop that dll into your resources directory and change your localization to de-DE and the new text should show up.

If you could not tell there are a lot of hoops to get through doing localization this way. Personal, I think the process is pretty ridiculous. I have no idea why someone would do localization this way instead of using resx files. I see only cons to this approach and no advantages over the resx approach. If you have a different opinion I would really like to hear your thoughts.

Saturday, April 26, 2008

WPF Localization - RESX Option

About a year ago I was building a WPF project in .Net 3.0 and Visual Studio 2005. I wanted to revisit this subject and see what has changed in .Net 3.5 and Visual Studio 2008. I will make a few of these posts to try and cover all the different options (RESX option, LocBaml option, Resource Dictionary Option). In this blog I will focus on using a resx file to localize an application.

To show how the resx option is done I created a WPF form with three labels on it. The first label has is text set inline in XAML, the second has it text set via code behind from the resx file and the third has its text set via XAML accessing the resx file.

The first thing that needs to happen to setup a project for localization is a small change to the project file. To make this change you will need to open the project file in notepad (or some other generic editor). In the first PropertyGroup section you need to add the follow XML node <UICulture>en-US</UICulture>. So the project file node would look like this:

image 

 

 

Adding this to the project file config will cause the IDE to create a resource.dll when you compile. Now we need to create our resx file. I like to create a Resources folder to keep my resx files in (You will have to create a resx file for each language you want to have). I created three labels in my XAML to show how this will work.

<Window x:Class="WpfLocalization.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"    
    xmlns:properties="clr-namespace:WpfLocalization.Resources">
    <Grid>
        <Label Height="28" Margin="10,13,33,0" 
               Name="lblFromXAML" Content="Text from XAML" 
               VerticalAlignment="Top"></Label>
        <Label Height="28" Margin="10,43,33,0" Name="lblFromResource" 
               VerticalAlignment="Top">Label</Label>
        <Label Height="28" Margin="10,71,33,0" Name="lblXAMLResource" 
               VerticalAlignment="Top"
               Content="{x:Static properties:UIStrings.lblXAMLResource}"></Label>
    </Grid>
</Window>

The first label is just used to show that text in the XAML does not change when we change the culture. The second label will be changed in code using the following:

public Window1()
{
    //UIStrings.Culture = new System.Globalization.CultureInfo("de-DE");
    InitializeComponent();
    
    lblFromResource.Content = UIStrings.lblFromResourcesText;
}

Pretty simple code. You will notice the third label has a binding in the content to do this. In my opinion this is the best way to do this as all your UI display is isolated to the XAML. However, I realize that in an enterprise application you will probably need code to decided what text is displayed (this could be accomplished using a trigger in  your style but that discussion is for another time). In order to do this binding though you have to include the xmlns import to your resx file. You will note that I imported the clr-namespace of WpfLocalization.Resources which is the namespace of my resx file. I can then bind to the static resource of properties:UIStrings.lblXAMLResource which pulls the text for my lblXAMLResource label (I gave my resource string and my label the same name).  When I run the application here is what I get.image Now I can copy my current resx file of UIStrings.resx and make a new resx file called UIStrings.de-DE.resx. I can then edit the text in this new resx file for my German text. The next time I build the compiler will create a secondary resource.dll file for de_DE. If I un-comment the line of code that is commented out above (this code changes the culture form the default of US to de-DE which is Germany) and run the application again I get different text as the resx resource that is used is for my de-DE culture. image I have uploaded the source code for this example as well if you would like to see the entire solution.

You will notice that at the top of the resx editor you have the ability to select rather or not the values in the resx are internal or public. This is new to .Net 3.5 and Visual Studio 2008. In Visual Studio 2005 you could not make these values public. They were always private and there for not accessible to the XAML file via the binding method or accessible to other projects in your solution.

Thursday, April 24, 2008

Excel XIRR and C#

I have spend that last couple days trying to figure out how to run and Excel XIRR function in a C# application. This process has been more painful that I thought it would have been when started. To save others (or myself the pain in the future if I have to do it again) I thought I would right a post about this (as post about XIRR in C# have been hard to come by).

Lets start with the easy part first. In order to make this call you need to use the Microsoft.Office.Interop.Excel dll. When you use this dll take note of what version of the dll you are using. If you are using a version less then 12 (at the time of this writing 12 was the highest version) you will not have an XIRR function call. This does not mean you cannot still do XIRR though. As of version 12 (a.k.a Office 2007) the XIRR function is a built in function to Excel. Prior version need an add-in to use this function. Even if you have version 12 of the interop though it does not mean you will be able to use the function. The ability to use the function is determined by the version of Office install on the machine hosting the application. So if your interop dll version is 12 but the host machine only has version 11 installed you cannot directly call the XIRR function even though the Interop dll gives you that method. If however the host machine does have office 12 installed you can call the XIRR function directly.

Ok lets get into the details of doing it. First add the Microsoft.Office.Interop.Excel.dll into your project references (It is best to import the latest version of the dll that you have).  Now create your excel object as in the code below:

// value to store the results
double xirrValue;

// create the new excel interop object 
ApplicationClass xlApp = new ApplicationClass();

Now we get to the tricky part (or at least it was for me). There are really two things we are going to check here. First we need to check what version of office is installed. If the version of office is greater than 11 the call is much easier. If the version of office is less then 11 we have more work to do.

Since versions of office before 12 don't include the XIRR function we have to register the Analysis tool box add-in. To register the add-in we make the xlApp.RegisterXLL call in the code below. Once the add-in has been register we can make a call to the XIRR macro using the Run() method.

Now there is one last issue to get by. The Run() method take something like 30 optional parameters. Now if you were doing this in VB.net this would not be an issue as you can just not pass in those extra optional parameters. However, for C# you have to pass something in for each parameter. You may think you can pass in null for these extra parameters but you would be wrong. You have to pass in System.Reflection.Missing.Value (thanks to Gabhan Berry for setting me straight on this).

// See what version of Excel is installed
if (Decimal.Parse(xlApp.Version) > 11)
{
    xirrValue = xlApp.WorksheetFunction.Xirr(_cash.ToArray(), _dates.ToArray(), 1);
}
else
{
    xlApp.RegisterXLL(xlApp.Path + @"\Library\Analysis\ANALYS32.XLL");
    xirrValue = (double)xlApp.Run("XIRR", _cash.ToArray(), _dates.ToArray(), 1,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value,
        System.Reflection.Missing.Value);
}

The rest of the code example above shows you what else needs to be done to get you there.