Friday, November 2, 2018

Writing and refactoring C# code: Translated text entries in Xamarin.Forms



The purpose of this article is to show how to write C# code and how to continually refactor the code and end up with clean code and a clean architecture. And with "clean" I mean that the resulting code and architecture are easy to understand, maintain, change and extend.


The example I will use here is creating and using translated text entries in Xamarin.Forms.

My goal is to not use resource files (.resx), but plain old C# code. There are some advantages to this approach that I will explain along the way. I’ll take baby steps and refactor the code as we go.


My usual "modus operandi" when writing code is to use Test Driven Development. However, because I don't know yet where I can put a "seam" between the framework code - which can't be unit tested - and the testable code, I will use an exploratory approach. I plan to write unit tests afterwards and keep testability in mind when writing the code.


Let’s start by creating a Blank Forms App. We’ll name the project TipsyTiger, because alliterating names are cool and memorable.




In the Solution options go to ".NET naming policies" and check the boxes so that namespaces match the directory structure.




The default MainPage.xaml that Xamarin creates for us looks like this:



As you see the lines don’t fit on the screen. Also, it has a useless comment, so let's format
the XAML code to this:



Much better. Let's run the app to check if everything is working as expected.



That’s what we expected to happen, so that’s great!

Now, let’s try and turn that “Welcome to Xamarin.Forms” text into a text entry that comes from
a translated file that can handle German, Dutch and English. If the user runs this on a device which has a default language that is not German, Dutch or English, it should default to English.

We need to create a so-called "extension" which we can use in our XAML code. We’ll call it TranslateExtension and put it into a folder called “Extensions” in the Xamarin.Forms project, as we expect to have more of these extensions in the future.



To make an extension work it has to implement the IMarkupExtension extension, which is part of the Xamarin.Forms framework. Type it in, select the IMarkupExtension text and press Alt+Enter to show the refactoring options. Select “Implement interface”:



For now let's have the ProvideValue return a hardcoded value. We will fix that later.

The extension requires a ContentProperty attribute. We’ll call it “Key”, because it will contain the key of the text resource we want to look up. Then we need a public string property that is named “Key”, so that Xamarin.Forms knows how to access it from the XAML code. The value of the ContentProperty and the name of the public property need to be the same, otherwise Xamarin.Forms will complain about it, and it won’t work.

We end up with this:



We need to declare the Extensions namespace In our MainPage.xaml file before we can use it. Then we can point the text of the label to our TranslateExtension, like this:



Xamarin.Forms will add "Extension" to the extension name, so in our case it maps the "extensions:Translate” to the TranslateExtension.

The key “WelcomeToXamarinForms” could be anything right now, because the TranslateExtension doesn't do anything with the key yet, but we will hook it up to something meaningful later.


This should work, but let’s be sure by running the app in the simulator.


Still working, good!

Now we want the TranslateExtension to use the given key and look up a corresponding text translated to the current UI language of the device.

The TranslateExtension has served us well so far, but I feel this particular functionality belongs to another class. Moreover, it’s an opportunity to break out of the Xamarin.Forms framework and get our code into plain old C# classes, which we can unit test.


There are several ways to go about this, but what we will do here is create a new .NET standard
library which we will call “Localization”. This library will contain the functionality for providing translated texts, and maybe some other localization duties in the future.

Rename the default "Class1.cs" in the "Localization" project to "Translator.cs".

The IDE asks us if we also want to rename the file, which of course is a great idea.




Back in our TranslateExtension create an instance of the Translator, and add a reference to our Localization project.

In our ProvideValue method we’ll pass the value of the Key into a Get method of the Translator. Our code now looks like this:


The Get method has a squiggly red line underneath it. Press Alt+Enter on it and
select Generate Method.


In the Translator class change the return type of the Get method to string. Again, we’ll return a fixed string for the time being.


You may think we’re just moving code around, and not really achieving anything. But we need this scaffolding to create a space for ourselves where we can put the right functionality.

Let's get to the meat. In our Translator class create a public property called "Text" of type "IText". 

The reason for making a public property is that - if we don’t access the translator through the TranslateExtension, but from, say, code behind or other plain old C# code - we can use this Text property and access it’s properties directly rather than through the Get method by the name of the key. The advantage of this is that correctness is checked during compile time rather than runtime.

Press Alt+Enter on the IText and create a new interface.



Change the return value of the Get method to a property of the IText interface. What should we call it? 

Do you remember the value of the key that we entered in our MainPage.xaml somewhere at the beginning of this article? It was “WelcomeToXamarinForms”. What we are trying to achieve is matching the key defined in the XAML to the text of the property of the same name in the IText interface. 

Press Alt-Enter on the squiggly line and generate a new read-only property in the interface.



The classes that will contain the actual translated texts will implement the IText interface. Let's create "EnglishText", "GermanText" and "DutchText" classes that implement the IText interface. 

Here’s how the GermanText class looks like:



Let’s go back to our Translator class. We have to create an instance of the right Text class, based on the current UI language. For example, if the two-letter ISO language code is "de" our app should display German text. 

Let’s create the constructor of the Translator. A nice shortcut to do this is to type “ctor” followed by a Tab. We’ll put the creation of the Text class into a separate method, to keep the constructor code simple and clean.


If I was writing this project TDD style, determining the language in the Translator class would make it harder to make this class testable. 

Checking the UI language feels like a thing we should delegate to the classes in the framework. Let’s move the determination of the UI language to the Xamarin.Forms layer, in the TranslateExtension class, and inject the resulting language code into the Translator class.


At this point we have written a lot of code without checking if everything still works. So let's start the app and check if it does (it should).

In our Translator class, we still don’t check the actual key. We still return the “Welcome to Xamarin.Forms!” string, even though it now does come from a translated source. To make this work, we need to match the value of the “key” to a property of the same name of the Text class. For this we need reflection.

See the GetPropertyValue method:



We are getting there. Let’s check if the actual translated value if retrieved by setting our device to German (Deutsch) and running the app.




Everything is working, but there are a few things we can do to make the code more robust, and clean it up a little more.

If we try to retrieve the string of a key that doesn’t exist, our app will crash on the reflection code with a null reference exception. Since the keys may be entered in XAML and these are not checked during compile time, it is possible that a key is used through XAML that doesn’t exist in the IText interface. 

We must decide what should happen in that case. A solution is to simply return the default value of a string (which is null) from the reflection method. Xamarin.Forms displays null values from the ProvideValue method as empty strings, so we’re good there. Let's make that change.

The GetPropertyValue method now looks like this:



It feels like the low level reflection code doesn't belong in our Translator class. It’s a generic method, so we can move this from the Translator to a separate ReflectionExtension class as an extsion method on an object. 

Create a static "ReflectionClass.cs" in the Localization project. Copy the GetPropertyValue method to it and turn it a static extension method that works on object types.

At some point in the future, it might make sense to move such a generally useful class to a Common project where all other projects may access it. 

Here is the resulting ReflectionExtension class:


The Translator class needs to modify slightly to call the GetPropertyValue on the Text object, instead of passing it as a parameter. Since the call is a single line we’ll make use of the lambda expression to simplify the code.




Reviewing the code another optimalisation pops out. In the TranslateExtension we get the strings through the Translator. However, with the GetPropertyValue method now being a public method we can do this more directly:




This makes the Get method in the Translator obsolete, so let’s delete it.





Looking at the Translator class now, it feels silly to pass the languageCode in the constructor, then set a Text property which consumers can access. 

We can make this more direct by removing the constructor and the Text property, and making the CreateText method public.

This is the result:



Amazing how much simpler that code turned out.

The TranslateExtension then changes to the following:



One spot of bother here is that if we start using the translator in other places - which is very plausible - we will have to copy that line of code to each of those places. If later we decide to change how the text is created - for example when we decide to use the CurrentUICulture’s Name property instead of its TwoLetterIsoLanguageName - then we need to change that in all those places as well. We can circumvent that by moving the creation of the Text object to a class of its own: a factory.


Let’s create a folder in the Xamarin.Forms project named “Factories” and create a class in that folder called “TextFactory”. Make the class static and create a public static method called CreateText and copy the line that create the Translator from the TranslateExtension to it. Let’s use the lambda operator to keep our code as simple as possible.




Then in the TranslateExtension call the CreateText method on the TextFactory instead:



Now, wherever we want to create a Text instance we just call the CreateText on the TextFactory. The TextFactory knows how to create the Text class. Other classes don’t need to know about that.

This is the final factoring of our code. We’re done for now (but run the app one more time to be sure J).

In a clean architecture every part should have only a single reason to chance. What parts in our architecture would need to change if a language is added, or a text entry is added?

If we add a language we need to add it to the switch statement in the Translator, and create a new Text class that implements the IText interface.

If we want to add a text entry we need to extend the IText interface and implement it in all the Text classes. The nice thing about that is that we can’t accidentally forget to add an entry in a language, because the compiler will remind us about it with an error. If we use resources files, we will find out about missing entries only at runtime.

Recap

All in all I'm happy with how this turned out. Still, there are a few things that might need some extra consideration:

- Throwing an exception in the GetPropertyValue class is a straightforward, but fairly expensive solution. We might think about checking the obj, type and property on null before using them, rather than just letting it throw an Exception

- In hindsight I might have created a "Text" folder in the Xamarin.Forms project and put the TranslateExtension and TextFactory there, instead of in an "Extensions" and "Factories" folder, respectively. This way all the Text related stuff is bundled together instead of spread over the project in separate folders.

- We still need to write unit tests :) 


Conclusion

In this article I have guided you through a series of steps on how to implement translation functionality in Xamarin.Forms, and how to call it from XAML through an Extension. I have shown you how to refactor the code along the way to end up with a clean and efficient solution.

No comments:

Post a Comment