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>

0 komentarze:

Prześlij komentarz

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