Pokazywanie postów oznaczonych etykietą Windows-Phone. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Windows-Phone. Pokaż wszystkie posty

niedziela, 22 lipca 2012

[EN] Windows Phone - skinnable application

1. Introduction
Hi
Today, I would like to present my concept of making skinnable application in Windows Phone 7.1. Making a skinnable application in Windows Phone might be a little bit tricky. Unfortunately, WP7 doesn't support DynamicResource keyword which is a basic tool for making skins in WPF. In order to overcome this inconvenience I decided to write a SkinManager for WindowsPhone.
2. Base view
As I mentioned before, there is no support for using dynamic resources in WP7, therefore, in order to force a control to change style, we have to assign this style in code behind. It is rather obvious that in a single window/control there might be quite a lot of other controls whose style also should be changed. That is why, I decided to write controls iterator first.
public class ViewsEnumerator : IEnumerator<FrameworkElement>
    {
        
        private Stack<FrameworkElement> _frameworkElementsStack = new Stack<FrameworkElement>();
        private FrameworkElement _current;
        private readonly FrameworkElement _startingElement;
        public ViewsEnumerator(FrameworkElement stargingElement)
        {
            _startingElement = stargingElement;
        }
        public void Dispose()
        {
            _current = null;
            _frameworkElementsStack.Clear();
            _frameworkElementsStack = null;
        }
 

        #region Implementation of IEnumerator

        public bool MoveNext()
        {
            if (_current == null)
            {
                _current = _startingElement;
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(_current); i++)
                    _frameworkElementsStack.Push((FrameworkElement)VisualTreeHelper.GetChild(_current, i));
            }
            else
            {

                if (_frameworkElementsStack.Count == 0) return false;
                _current = _frameworkElementsStack.Pop();

                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(_current); i++)
                    _frameworkElementsStack.Push((FrameworkElement)VisualTreeHelper.GetChild(_current, i));
                return true;
            }
            return true;
        }

        public void Reset()
        {
            _current = null;
            _frameworkElementsStack.Clear();
            _frameworkElementsStack = null;
        }

        public FrameworkElement Current
        {
            get { return _current; }
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        #endregion
    }
In this iterator I used DFS algorithm for searching elements in a tree. Thanks to this piece of code I can take advantage of LINQ's benefits. In the next step I created base classes for views.
public interface IViewBase : IEnumerable<FrameworkElement>
    {
        
    }

 public class ViewBase : UserControl, IViewBase
    {
       #region Implementation of IEnumerable

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<FrameworkElement> GetEnumerator()
        {
            return new ViewsEnumerator(this);
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }

3. SkinManager class
Having prepared basis for skinnable control, let me introduce a custom attribute called the SkinnablePartAttribute
[AttributeUsage(AttributeTargets.Class)]
    public class SkinnablePartAttribute : Attribute
    {
        public string ThemePartName { get; set; }

    }
This attribute should decorate custom controls which should have an ability to change theme/skin. As you can see, the SkinnablePartAttribute has a ThemePartName property which holds information about a name of ResourceDictionary where styles for the given control ought to be searched. Now, it is time to take a look at a SkinManager class. The SkinManager class has one public function - ApplySkin
public static void ApplySkin<TView>(TView view, string skinName)
           where TView : FrameworkElement, IViewBase
        {

            foreach (IViewBase control in view.Where(val => val.ImplementsInterfaceOf<IViewBase>()))
            {
                var assemblyName = control.GetType().Assembly.FullName;
                Uri parsedUri;
                var attribute = control.GetType().GetCustomAttributes(typeof(SkinnablePartAttribute), true).SingleOrDefault();
                if (attribute != null && Uri.TryCreate(string.Format("/{0};component/Themes/{1}/{2}.xaml",assemblyName.Substring(0, assemblyName.IndexOf(",", StringComparison.Ordinal)), skinName,(attribute as SkinnablePartAttribute).ThemePartName), UriKind.Relative, out parsedUri))
                    ApplySkin(control, parsedUri);
                else
                    Debug.WriteLine(string.Format("Could not find {0}/{1}", skinName,(attribute as SkinnablePartAttribute).ThemePartName));
                
            }
        }
This function iterates through all controls which implement a IViewBase interface - in other words, the function iterates through our custom controls. If a control is decorated with the SkinnablePartAttribue, a new Uri is created based on the information from the ThemePartName property and from the skinName parameter(I assumed that all dictionaries are located in the Themes directory). In the final step, private function also known as the AppySkin function is called. This function looks this way:
private static void ApplySkin<TView>(TView view, Uri uri, int mergedDictionaryIndex = 0)
            where TView :  IViewBase
        {
            var castedView = view as FrameworkElement;
            if(castedView == null) return;

            var resourceMergedDictionary = castedView.Resources.MergedDictionaries[mergedDictionaryIndex];
            var beforeClearDictionary = resourceMergedDictionary.ToDictionary(key => key.Value, val => new { key = val.Key, value = val.Value });
            castedView.Resources.MergedDictionaries.Remove(resourceMergedDictionary);
            resourceMergedDictionary.Clear();

            var newStyleDictionary = new ResourceDictionary
            {
                Source = uri
            };

            castedView.Resources.MergedDictionaries.Insert(mergedDictionaryIndex, newStyleDictionary);
            foreach (var control in view.Where(val => val.Style != null && !val.ImplementsInterfaceOf<IViewBase>()).Where(control =>
                beforeClearDictionary.ContainsKey(control.Style) && newStyleDictionary[beforeClearDictionary[control.Style].key] != null))
                control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;

            beforeClearDictionary.Clear();

        }
Here, all the magic begins. In the first step, current ResourceDictionary is transformed into the standard dictionary. The key of this dictionary is a value of style – a reference to the style object. In the next step, I created a new ResourceDictionary based on the Uri parameter passed to this function. In the last step, another loop over controls is performed. However, this time only controls with the assigned Style property are taken into consideration. Then, if the newStyleDictionary contains a key of given control style we are ready to swap styles.
control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;
Entire listing of SkinManager is presented below
public static class SkinManager
    {
        private static void ApplySkin<TView>(TView view, Uri uri, int mergedDictionaryIndex = 0)
            where TView :  IViewBase
        {
            var castedView = view as FrameworkElement;
            if(castedView == null) return;

            var resourceMergedDictionary = castedView.Resources.MergedDictionaries[mergedDictionaryIndex];
            var beforeClearDictionary = resourceMergedDictionary.ToDictionary(key => key.Value, val => new { key = val.Key, value = val.Value });
            castedView.Resources.MergedDictionaries.Remove(resourceMergedDictionary);
            resourceMergedDictionary.Clear();

            var newStyleDictionary = new ResourceDictionary
            {
                Source = uri
            };

            castedView.Resources.MergedDictionaries.Insert(mergedDictionaryIndex, newStyleDictionary);
            foreach (var control in view.Where(val => val.Style != null && !val.ImplementsInterfaceOf<IViewBase>()).Where(control =>
                beforeClearDictionary.ContainsKey(control.Style) && newStyleDictionary[beforeClearDictionary[control.Style].key] != null))
                control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;

            beforeClearDictionary.Clear();

        }
        public static void ApplySkin<TView>(TView view, string skinName)
           where TView : FrameworkElement, IViewBase
        {

            foreach (IViewBase control in view.Where(val => val.ImplementsInterfaceOf<IViewBase>()))
            {
                var assemblyName = control.GetType().Assembly.FullName;
                Uri parsedUri;
                var attribute = control.GetType().GetCustomAttributes(typeof(SkinnablePartAttribute), true).SingleOrDefault();
                if (attribute != null && Uri.TryCreate(string.Format("/{0};component/Themes/{1}/{2}.xaml",assemblyName.Substring(0, assemblyName.IndexOf(",", StringComparison.Ordinal)), skinName,(attribute as SkinnablePartAttribute).ThemePartName), UriKind.Relative, out parsedUri))
                    ApplySkin(control, parsedUri);
                else
                    Debug.WriteLine(string.Format("Could not find {0}/{1}", skinName,(attribute as SkinnablePartAttribute).ThemePartName));
                
            }
        }
4. Example of usage
In order to use the SkinManager it is essential to make some preparation. First of all, you have to create Themes directory in a project. In this directory there must be separate subdirectories for every skin you would like to use. What is more, in these subdirectories you should place all ResourceDictionaries specified in the SkinnablePartAttribute. A sample directory structure is shown in the picture below:
The very last step is to make controls (which should be skinnable) inherit from ViewBase. Here is example of usage
namespace Joomanji.Chat.Client.Views
{
    [SkinnablePart(ThemePartName = "ChatWindowSkin")]
    public partial class ChatWindowView : ViewBase
    {
        public ChatWindowView()
        {
            SkinManager.ApplySkin(this, "Red"); // change skin
        }
    }   
}
and screenshots of the same view with different skin
5. Controls with dynamically changing content
After some test, it turned out that the mechanism presented above has problems with controls which change their content dynamically. Let me clarify - styling grids, list boxes etc. work fine, however changing row styles and listboxitem styles are not working properly. In order to overcome this problem you must define a style for a specific target type. Here is an example of usage with the listbox:
<Style TargetType="ListBox">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Setter Property="VerticalAlignment" Value="Stretch"/>
                        <Setter Property="ItemTemplate" Value="{StaticResource GeneralChatMessageTemplate}"/>
                    </Style>

poniedziałek, 7 maja 2012

Tworzenie bootstrappera aplikacji przy użyciu CaliburnMicro

Witam po długiej przerwie. W dzisiejszym wpisie postaram się krótko opisać w jaki sposób stworzyć bootstrapper aplikacji przy użyciu CaliburnMicro. Ponadto przedstawię w jaki sposób skonfigurować bootstrapper w taki sposób, aby Caliburn wykorzystywał nasz własny kontener IOC.

1. Wstęp

CaliburnMicro jest to framework MVVM, który w znacznym stopniu ułatwia i przyśpiesza pisanie aplikacji pod WPF,Silverlight,WindowsPhone oraz WinRT. Zdecydowałem się poznać ten framework z uwagi na jego przenośność na różne platformy. Wcześniej całkiem sporo czasu poświęciłem Prismowi, jednakże z powodu licznych problemów z jego działaniem pod WindowsPhonem chciałem spróbować czegoś nowego.

2. Bootstrapper - Silverlight

Bootstrapper jest to ogólnie rzecz biorąc klasa inicjalizująca całą aplikację. W trakcie odpalania bootstrappera na ogół konfiguruje się kontener IOC(rejestruje wszystkie potrzebne typu), inicjalizuje się połączenie z serverem, oraz odpala się główne okno aplikacji - tzw. Shella. Dlatego też musimy odchudzić plik App.xaml.cs i zostawić w nim jedynie konstruktor wraz z wywołaniem funkcji InitializeComponents()
 public partial class App : Application
    {
        public App()
        {          
            InitializeComponent();
        }
    }
Zacznijmy od utworzenia nowego projektu typu SilverlightApplication, do którego dodajemy referencje do dll-ek Cliburna. Możemy to zrobić poprzez NuGetta (niestety u mnie one nie działały :D), jak również możemy je ściągnąć z oficjalnej strony projektu http://caliburnmicro.codeplex.com/releases/view/81466. Następnie stwórzmy główne okno aplikacji(wspomnianego wcześniej Shella) oraz jego viewmodel. W tym celu dodajemy do projektu Silverlight User Control o nazwie ShellView
oraz nową klasę ShellViewModel.
Dorzućmy jeszcze jakiegoś TextBox-a do naszego ShellView, tak aby mieć pewność, że rzeczywiście odpowiednie okno jest widoczne przy starcie aplikacji
<UserControl x:Class="Loginv2.ShellView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro" mc:Ignorable="d" Height="516" Width="792">

    <Grid x:Name="LayoutRoot"  >
        <TextBox Text="oto jest shell"/>
    </Grid>
</UserControl>
W kolejnym kroku musimy stworzyć właściwy bootstrapper - dodajmy nową klasę o nazwie SilverlightBootstrapper, która dziedziczyć będzie po klasie Bootstrapper
namespace Client.Silverlight
{
    public class SilverlightBootstrapper : Bootstrapper<ShellViewModel>
    {
      
    }
}
Klasa Bootstrapper jest to klasa dostarczona przez CaliburnMicro, generyczny parametr T określa nam viewmodel na podstawie którego framework będzie wyszukiwał odpowiedni widok shella ze swojego wbudowanego kontenera IOC (Caliburn podczas uruchomienia aplikacji rejestruje dostępne typu w kontenerze). Zgodnie z domyślną konwencją, CaliburnMicro jako główne okno aplikacji ustawi widok, który nazywać się będzie ShellView. W celu odpalenia naszego bootstrappera musimy jeszcze umieścić go w zasobach aplikacji. Zmodyfikujmy zatem plik App.xaml aby wyglądał w następujący sposób
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Silverlight="clr-namespace:Client.Silverlight" x:Class="Client.Silverlight.App"
             >
    <Application.Resources>
        <Silverlight:SilverlightBootstrapper x:Key="Bootstrapper"></Silverlight:SilverlightBootstrapper>
    </Application.Resources>
</Application>
Kompilując, a następnie uruchamiając nasz projekt naszym oczom powinien ukazać się następujący widok


2.1 Własny kontener IOC

Powyżej przedstawiłem w jaki sposób stworzyć najprostszą wersję bootstrapera z wykorzystaniem CaliburnMicro oraz jego domyślnego kontenera IOC. Jednakże najczęściej jest tak, że w aplikacji wykorzystujemy już jakiś bardziej zaawansowany kontener i po prostu nie chcemy wykorzystywać jednego kontenera do "rozwiązywania" widoków, a drugiego do pozostałych rzeczy. Dlatego też pokaże teraz w jaki sposób skonfigurować napisany wcześniej bootstrapper tak aby widoki były wyciągane z naszego kontenera IOC. Zacznijmy do dodania do naszego projektu referencji do NInjecta, a następnie utwórzmy klasę IOCContainer (będącą naszym customowym kontenerem), wyglądającą w następujący sposób
public class IOCContainer
{
        private static readonly IKernel Kernel = new StandardKernel();

        protected IOCContainer()
        {

        }

        public static void Dispose()
        {
            Kernel.Dispose();
        }

        public static object Get(Type type)
        {
            return Kernel.Get(type);
        }

        public static object Get(Type type,string key)
        {
            return Kernel.Get(type, key);
        }
  
        public static IEnumerable<object>  GetAll(Type instanceType)
        {
            return Kernel.GetAll(instanceType);
        }
  
        public static void Bind<T, TS>() where TS : T
        {
            Kernel.Bind<T>().To<TS>();
        } 
    }
Następnie w klasie SilverlightBootstraper musimy przeciążyć funkcję Configure,GetAllInstances oraz GetInstance. Jako, że będziemy korzystali z naszego własnego kontenera w funkcji Configure rejestrujemy wszystkie potrzebne nam viewmodele oraz widoki. Zatem funkcja Configure powinna od teraz wyglądać tak
protected override void Configure()
        {
            IOCContainer.Bind<ShellView,ShellView>();
            IOCContainer.Bind<ShellViewModel,ShellViewModel>();
        }
Następnie musimy "pokazać" Caliburn-owi gdzie powinien szukać widoków. Dlatego też przeciążamy funkcje GetInstance oraz GetAllInstances i zmieniamy ich postać na następującą
protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }
Od tej pory, za każdym razem gdy użyjemy mechanizmów Caliburna do bindowania viewmodelu z widokiem itp.,Caliburm będzie szukał widoków oraz viewmodeli w naszym kontenerze IOC.

2.2 Własna konwencja wyszukiwania widoków

W przedstawionym powyżej przykładzie bootstrappera, nasz bootstrapper korzystał z domyślnej konwencji rozwiązywania widoków na podstawie viewmodeli (do ShellViewModel został dopasowany widok ShellView). Czasem jednak konwencja ta, nie pasuje do konwencji przyjętej w danym projekcie. Sam biorę udział w projekcie, w którym widoki w kontenerze są rejestrowane następujący sposób
IOCContainer.Bind< IView<ViewModel>,View>
Czy zatem oznacza to, że nie mogę korzystać już mechanizmów Caliburna i musze zrezygnować chociażby z bootstrappera ?Oczywiście,że nie. Framework dostarcza nam możliwość zdefiniowania własnej konwencji wyszukiwania widoków na podstawie viewmodeli. W celu zastąpienia domyślnej konwencji musimy podpiąć się pod propercję LocateForModelType znajdującą się w klasie ViewLocator i zdefiniować własną funkcję wyszukującą widok na podstawie viewmodelu. W moim przypadku wygląda to w następujący sposób
ViewLocator.LocateForModelType = (modelType, displayLocation, context) =>
                                                 {

                                                     string viewTypeName = string.Format("{0}[[{1}]],{2}", typeof(IView<>).FullName, modelType.AssemblyQualifiedName, typeof(IView<>).Assembly.FullName);
                                                     Type viewType = Type.GetType(viewTypeName);
                                                     if (viewType == null)
                                                         throw new ArgumentNullException(string.Format("Nie odnaleziono typu widoku dla viewModelu"), modelType.FullName);

                                                     return ViewLocator.GetOrCreateViewType(viewType);
                                                 };
Funkcja ViewLocator.LocateForModelType odpalana jest za każdym razem, gdy CaliburnMicro chce wyszukać widok na podstawie viewmodleu. Najważniejszym argumentem funkcji LocateForModelType jest modelType - czyli typ viewmodelu, który posłuży nam do znalezienia widoku - a właściwie jego typu. Po odnalezieniu typu widoku, wywołujemy funkcję
ViewLocator.GetOrCreateViewType(viewType);
(które z kolei znajdzie nam w kontenerze pożądanym przez nas typ widoku) i zwracamy jej rezultat.

3. Bootstrapper - Windows Phone

W przypadku bootstrappera dla Windows Phona sytuacja wygląda odrobinę inaczej. W celu utworzenia własnego bootstrappera musimy rozszerzyć klasę PhoneBootstrapper. Utwórzmy zatem nowy projekt typu Windows Phone Application, oraz dodajmy do niego nową kontrolkę typu Windows Phone Portrait Page.
Podobnie jak w przypadku projektu Silverlightowego dodajemy referencję do CaliburnMicro ,dorzucamy klasę viewmodelu (którą nazywamy ShellViewModel) oraz dodajemy jakiegoś textboxa do ShellView. Następnie musimy utworzyć nasz bootstrapper, dodajmy zatem klasę WindowsPhoneBootstrapper rozszerzającą klasę PhoneBootstrapper. Podobnie jak w przypadku bootstrappera silverlightowego przeciążamy funkcje odpowiedzialne za konfigurację kontenera IOC.
public class WindowsPhoneBootstrapper : PhoneBootstrapper
    {
        protected override void Configure()
        {
          
           IOCContainer.BindToConstant<INavigationService>(new FrameAdapter(RootFrame));
           IOCContainer.BindToConstant<IPhoneService>(new PhoneApplicationServiceAdapter(RootFrame));
           IOCContainer.BindAsSingleton<ShellViewModel, ShellViewModel>();
        }

      
        protected override void OnExit(object sender, EventArgs e)
        {
            IOCContainer.Dispose();
            base.OnExit(sender, e);
        }

        protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }

        protected override void BuildUp(object instance)
        {
            IOCContainer.Inject(instance);
        }
    }
Zwróćmy uwagę, że w kontenerze IOC zostały zarejestrowane usługi nawigacji Windows Phona, do których został przekazany RootFrame. RootFrame jest to główne okno aplikacji, zostaje ono utworzone przez CliburnMicro. Tak samo jak w przypadku projektu Silverlightowego w pliku App.xaml.cs zostawiamy jedynie konstruktor z wywołaniem funkcji InitializeComponents(). W celu ustawienia ShellView jako RootFram-a musimy zmodyfikować plik WMAppManifest.xml (znajdujący się w katalogu Properties). Odnajdujemy tam wpis
<DefaultTask  Name ="_default" NavigationPage="MainPage.xaml"/>
i zamieniamy go na
<DefaultTask  Name ="_default" NavigationPage="ShellView.xaml"/>
Jak widać WindowsPhone wymusza tutaj podejście ViewFirst. Przy uruchomieniu aplikacji mechanizm nawigacji WindowsPhona odpali widok ShellView (bez wiedzy Caliburn'a), natomiast CaliburnMicro dopasuje odpowiedni ViewModel do naszego widoku - na podstawie domyślnej konwencji. Jeżeli chcielibyśmy zmodyfikować sposób wyszukiwania ViewModeli do widoków musimy podpiąć się do propertisa LocateForViewType znajdującego się w klasie ViewModelLocator
 ViewModelLocator.LocateForViewType = type =>
                                                     {
                                                         return //tutaj zwroc odpowiedni viewmodel na podstawie parametru type ;
                                                     };
W ostatnim kroku musimy jeszcze tylko dodać nasz bootstrapper do zasobów aplikacji
<Application.Resources>
        <Bootstrapper:WindowsPhoneBootstrapper x:Key="Bootstrapper"></Bootstrapper:WindowsPhoneBootstrapper>
    </Application.Resources>
odpalając teraz program naszym oczom powinien ukazać się taki oto widok

sobota, 21 stycznia 2012

Lokalizowanie aplikacji Silverlight i Windows Phone z wykorzystaniem Portable Shared Library

Witam
W dzisiejszym wpisie postaram się przedstawić w jaki sposób lokalizować treści aplikacji typu Silverlight oraz WindowsPhone. Założenie jest takie, że chcemy zbudować multiplatformową aplikację, która będzie obsługiwała platformę Silverlight oraz Windows Phone. Rozsądnym zatem podejściem jest trzymanie wszystkich tłumaczeń w jednym miejscu - najlepiej aby mechanizm tłumaczenia był obsługiwany zarówno przez Windows Phona jak i Silverlighta.
W pierwszym kroku musimy doinstalować do środowiska Visual Studio projekt typu Portable Class Library. Referencje do tego typu projektów możemy dodawać zarówno do projektów Silverlightowych, WindowsPhonowych - zatem idealnie nadaje się na przetrzymywanie w nim mechanizmu tłumaczenia aplikacji. W celu zainstalowania wyżej wspomnianego typu projektu musimy odwiedzić następującą stronę
http://visualstudiogallery.msdn.microsoft.com/b0e0b5e9-e138-410b-ad10-00cb3caf4981, a następnie zassać plik instalacyjny. Mając już przygotowane środowisko, stwórzmy nową solucję i dodajmy do niej projekt typu Portable Class Library - nazwijmy go SharedPortableClasses.

Następnie dodajmy do tego projektu nowy folder "Localization", w którym to umieścimy pliki typu *.resx. Pierwszy plik nazywamy po prostu Localization.resx natomiast następne nazywamy według wzorca Localization.<culture>.resx, gdzie:

  • <culture> - oznacza kulturę/język komunikatów jaki dany plik będzie przechowywać


    Dla przykładu aby mechanizm tłumaczący obsługiwał język polski oraz angielski, powinniśmy stworzyć następującą strukturę plików
  • Localization.resx

  • Localization.pl-PL.resx

  • Localization.en-US.resx



W kolejnym kroku musimy zmienić modyfikator dostępu do naszych plików zasobów na publiczny. W tym celu klikamy podwójnie na każdy plik *.resx i z comboboxa wybieramy public

Mając przygotowane pliki zasobów, możemy teraz umieszczać w nich tłumaczone elementy interfejsów oraz komunikatów. Robimy to w następujący sposób:

  • W pliku Localization.resx wypełniamy pole w kolumnie "Name" jakąś wartością(kluczem), która identyfikuje nasz tekst (w moim przypadku będzie to "Title")
  • W plikach Localization.pl-PL.resx oraz Localization.en-US powtarzamy tę czynność (uzupełniamy pola w kolumnie "Name" tymi samymi wartościami co w pliku Localization.resx) oraz wypełniamy pole w kolumnie "Value" tekstem w odpowiednim języku.

Mając już prawie gotowy mechanizm tłumaczący stwórzmy teraz projekt typu "Silverlight Application" oraz dodajmy mu referencję do projektu SharedPortableClasses. Dodajmy teraz do zasobów naszej aplikacji obiekt typu Localization

<Application.Resources>
<Localization:Localization x:Key="LocalizationProxy" />
</Application.Resources>

Niestety z powodu tego, że klasa Localization nie posiada publicznego konstruktora bezparametrowego, nie uda nam się odpalić naszej aplikacji. Resharper od razu wyrzuci ostrzeżenie następującej treści

Zatem w jaki sposób wykorzystać nasz mechanizm tłumaczący ? Rozwiązania są dwa

  • Zmodyfikować plik Localization.Designer.cs i ustawić konstruktor domyślny na publiczny. Takie rozwiązanie jednak nie jest eleganckie, gdyż za każdym razem gdy dodamy nowy wpis do pliku Localization.resx kod w pliku Localization.Designer.cs zostanie przegenerowany.

  • Drugim sposobem jest stworzenie klasy pośredniczącej, która będzie w sobie trzymała instancję obiektu Localization, oraz dodanie obiektu tej klasy pośredniczącej do zasobów aplikacji


Jako, że podejście drugi jest wg mnie dużo lepsze to postaram się je teraz przedstawić. Do projektu SharedPortableClasses dodajmy klasę LocalizationProxy.cs.

public class LocalizationProxy : INotifyPropertyChanged
{
public Localization LocalizationManager { get; private set; }

public LocalizationProxy()
{
LocalizationManager = new Localization();
}

public void ResetResources()
{
OnPropertyChanged("LocalizationManager");
}


#region INotifyPropertyChanged region
public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}


#endregion
}

Teraz stwórzmy instancję tej klasy w zasobach aplikacji

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Localization="clr-namespace:SharedPortableLibrary.Localization;assembly=SharedPortableLibrary"
x:Class="LocalizationSolution.App"
>
<Application.Resources>
<Localization:LocalizationProxy x:Key="LocalizationProxy" />
</Application.Resources>
</Application>

Zanim przystąpimy do tłumaczenia samego interfejsu musimy jeszcze zmodyfikować plik projekty *.csproj. Otwieramy nasz plik projektu w dowolnym edytorze tekstowym, a następnie w sekcji SupportedCultures dorzucamy kultury w jakich
nasza aplikacja ma pracować. W moim przypadku będzie to wyglądać w następujący sposób:

Jak widać moja aplikacja będzie wspierała język angielski oraz język polski. Mając już wszystko gotowe przystąpmy do tworzenia interfejsu. Zmodyfikujmy plik MainPage.xaml tak aby wyglądał w następujący sposób

<UserControl x:Class="LocalizationSolution.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock TextWrapping="Wrap" x:Name="PageTitle" Text="{Binding Source={StaticResource LocalizationProxy},Path=LocalizationManager.Title,Mode=TwoWay}" Margin="9,-7,0,0" />
<Button Content="Polski" Command="{Binding PolisLanguageCommand}" Height="72" HorizontalAlignment="Left" VerticalAlignment="Top" Width="160" />
<Button Content="English" Command="{Binding EnglishLanguageCommand}" Height="72" HorizontalAlignment="Left" VerticalAlignment="Top" Width="160" />
</StackPanel>
</Grid>
</UserControl>

Widok ten jest bardzo prosty. Posada on dwa przyciski służące do zmiany aktualnego języka oraz labelkę, na której dany tekst będzie się pojawiać. Najciekawszą rzeczą w tym kodzie jest sposób wyświetlania tłumaczonego tekstu. Jak widać do właściwości Text elementu typu TextBlok nie jest przypisywana wartość stała, ale używany jest binding

Text="{Binding Source={StaticResource LocalizationProxy},Path=LocalizationManager.Title,Mode=TwoWay}"

To co wyświetli się w danym TextBloku zależne jest od tego co zwróci nam nasz mechanizm tłumaczący. Odnosimy się do niego poprzez wskazanie zasobu, który nazwaliśmy LocalizationProxy (taki klucz nadaliśmy mu w zasobach aplikacji). Następnie do konkretnego tekstu odnosimy się podając odpowiednią ścieżkę - w tym przypadku jest to LocalizationManager.Title. LocalizationManager jest to właściwość, którą stworzyliśmy w klasie LocalizationProxy, natoamist Title jest to klucz jaki wpisaliśmy w pliku zasobów Localization.resx. Przeglądając plik Localization.Designer.cs można zauważyć, że została tam wygenerowana właściwość Title.

public static string Title {
get {
return ResourceManager.GetString("Title", resourceCulture);
}
}

Właściwość tak zwraca nam tekst w odpowiednim języku. Sam mechanizm zwracania tekstu dla wybranego przez nas języka jest już dostarczony z Frameworkiem. Tekst wyświetlany w aplikacji będzie w takim języku/kulturze jaka zostania przypisana do właściwości Thread.CurrentThread.CurrentUICulture. Możemy to w łatwy sposób sprawdzić zmieniając dynamicznie kulture wątku.
Stwórzmy nową klasę i nazwijmy ją MainPageViewModel. Wygląda ona w następujący sposób

public class MainPageViewModel
{
public DelegateCommand PolisLanguageCommand { get; set; }
public DelegateCommand EnglishLanguageCommand { get; set; }
public MainPageViewModel()
{
PolisLanguageCommand = new DelegateCommand(() =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("pl-PL");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("pl-PL");
ResetResources();

});

EnglishLanguageCommand = new DelegateCommand(() =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
ResetResources();
});
}

private void ResetResources()
{
((LocalizationProxy)Application.Current.Resources["LocalizationProxy"]).ResetResources();
}
}

Następnie stwórzmy obiekt tej klasy i przypiszmy go do DataContext-u okna MainPage.

public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
DataContext = new MainPageViewModel();
}
}

Dzięki wykorzystaniu komend po naciśnięciu przycisku "Polski" zostanie odpalana komenda PolishLangugeCommand, która z kolei odpali anonimową funkcję do niej przypisaną. W funkcji tej zmienimy kulturę wątku na "pl-PL". Ważnym elementem jest tutaj funkcja ResetResources(), która znajduje się w klasie LocalizationProxy. Funkcja ta informuje widok (poprzez zdarzenie NotifyPropertyChanged), że należy odświeżyć wszystkie elementy, które zostały zbindowane do właściwości LocalizationManager.

Postępując w analogiczny sposób możemy stworzyć aplikację WindowsPhone, która będzie wykorzystywałą nasz mechanizm tłumaczący. Co więcej dzięki temu, że dwie aplikacje będą korzystały z tego samego mechanizmu oraz z tych samych resourców, nie musimy podwójnie wpisywać tłumaczonych tekstów.



Projekt można znaleźć pod tym linkiem
http://www.4shared.com/rar/i9cd3EGD/LocalizationSolution.html

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management