Tuesday, March 25, 2008

FindResource replacement and how to change control style more then once in Silverlight 2.0 application

As deeper we’re digging in Silverlight 2.0, we finding more and more WPF things and we’re really missing in Silverlight. One of such things is FindResources.

In WPF I had Visual and Logical tree, so I was able to travel the tree to find resource I need. Let’s see an example of the application. We have one resource defined in App level

<Application.Resources>
        <Style TargetType="Button" x:Key="green">
            <Setter Property="Background" Value="Green"/>
        </Style>
</Application.Resources>

Another resources are defined in different levels of Page

<UserControl.Resources>
        <Style TargetType="Button" x:Name="red">
            <Setter Property="Background" Value="Red"/>
        </Style>       
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.Resources>
            <Style TargetType="Button" x:Name="blue">
                <Setter Property="Background" Value="Blue"/>
            </Style>
        </Grid.Resources>
        <Button Content="Click me" Click="Button_Click">
            <Button.Resources>
                <Style TargetType="Button" x:Name="yellow">
                    <Setter Property="Background" Value="#FFFFFF00"/>
                </Style>
            </Button.Resources>
        </Button>
    </Grid>

Now I want to call FindResource(“red”) and have my style ready for apply. I should not thing a lot about where the resource exists. There is no such method in Silverlight. If so, let’s see what we have. Looking in debugger I can find all my resources as members of the page.

image

But how to get them out? In Silverlight FrameworkElement, we have handy method named FindName. That’s exactly what we need. But how to get Application resources? Simple. Just look into it’s collection. Now, I can write small method, that help me to find resources in any level of Silverlight application.

public static object FindResource(string name)
        {
            if (App.Current.Resources.Contains(name))
            {
                return App.Current.Resources[name];
            }
            else
            {
                FrameworkElement root = App.Current.RootVisual as FrameworkElement;
                return root.FindResource(name);
            }
        }
        internal static object FindResource(this FrameworkElement root, string name)
        {
            if (root != null && root.Resources.Contains(name))
            {
                return root.Resources[name];
            }
            else
            {
                try
                {
                    return root.FindName(name);
                }
                catch { }               
            }

            return null;

Well now we can find all our resources. Let’s apply it to elements

Style s = (Style)Helper.FindResource("red");
b.Style=s;

It works perfect. Let’s take another one

Style s = (Style)Helper.FindResource("blue");
b.Style = s;

image

What is it? “Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))”. Why this happens only after applying second style? Let’s look into MSDN: “Styles are write-once in Silverlight. You can set a style to override a built-in default style, but attempting to set the same style again will result in an exception.”

What to do? We just have to write our own multi-use styling engine. First of all, we should get all setters of the style

foreach (Setter setter in value.Setters)

Then check target type and set values of setters to appropriate properties.

Type targetType = parent.GetValue(setter.Property).GetType();
parent.SetValue(setter.Property, setter.Value);

Another exception. That’s the mess? All values are strings. I need real values and I have no parsers. Fortunately, we have handy .NET method Convert.ChangeType. Let’s use it. Exception. Let’s check if the type is IConvertible (such as int, double, etc)

if(targetType is IConvertible)
            {
                return Convert.ChangeType(source, targetType, CultureInfo.InvariantCulture);
            }

No exception, but also no result. We need SolidColorBrush and we have only Color name, which is string. How to convert such string into Color instance and then into SolidColorBrush? The answer is reflection. We should write our own FromString converter. First of all let’s capitalize the string

static string Capitalize(this string str)
        {
            if (str.Length > 0)
            {
                return string.Concat(str.Substring(0, 1).ToUpper(), str.Substring(1, str.Length - 1));
            }
            return str;
        }

Then check whether I have static member with the same name in Colors class (not by hand of cause)

MemberInfo[] infos = typeof(Colors).GetMember(color.Capitalize());

If I have, let’s invoke it

if (infos.Length > 0)
            {
                return (Color)typeof(Colors).InvokeMember(color.Capitalize(), BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty, null, null, null);
            }

Well done. Now we can use strings which have names of colors and convert them into real color. But what to do with not “well known colors”? Parse it

else if (color.IndexOf('#') == 0)
            {
                return Color.FromArgb(
                    byte.Parse(color.Substring(1, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(3, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(5, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(7, 2), NumberStyles.HexNumber));
            }

We done. Just write another attached method and use in in our converter

internal static Color ParseKnownColor(string color)
        {
            MemberInfo[] infos = typeof(Colors).GetMember(color.Capitalize());
            if (infos.Length > 0)
            {
                return (Color)typeof(Colors).InvokeMember(color.Capitalize(), BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty, null, null, null);
            }
            else if (color.IndexOf('#') == 0)
            {
                return Color.FromArgb(
                    byte.Parse(color.Substring(1, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(3, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(5, 2), NumberStyles.HexNumber),
                    byte.Parse(color.Substring(7, 2), NumberStyles.HexNumber));
            }
            return Colors.White;
        }
……

else if(targetType == typeof(SolidColorBrush))
            {
                return new SolidColorBrush(ParseKnownColor(source));
            }

Now you turn to continue with string to object convention for your own need. Starting today you know how to do it.

Here the result

Have a nice day and be good people

Source code for this article>>

No comments: