Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Breaking apart the Windows Phone 8.1 ComboBox Style and Colors

Yesterday I had an interesting, if not a little frustrating, time pulling apart the default style of the Windows Phone 8.1 ComboBox. Before I start I’ve put together a series of images showing various states of the ComboBox:

A – The default unfocused state of the ComboBox, showing the placeholder text
B – The state when an item has been selected (focused and unfocused look the same)
C – The pressed state, prior to items selection showing
D – The expanded state when only a few items (5 or less) in the list using one of the default TextBlock styles (we’ll discuss this in a minute)
E – The expanded state when only a few items (5 or less) in the list using a TextBlock without the Foreground color set (no selection)
F – The expanded state when only a few items (5 or less) in the list using a TextBlock without the Foreground color set (item selected)
G – The disabled (ie IsEnabled=false) state
H – The expanded state when more than 5 items, which is same regardless of whether an item is selected or not (ie no selection shown)

image

 

Don’t Use Any of the Built In TextBlock Styles!!!

Before we jump into look at the ComboBox styles and colors in more detail, let me briefly discuss D in more detail. For this particular scenario the combobox has 3 items in it which means that the expanded view will show the items in situ (same behaviour as the ListPicker from Windows Phone 8/8.1 Silverlight). The ItemTemplate for the ComboBox looks like the following:

<DataTemplate x:Key="ItemsItemTemplate">
    <TextBlock Text="{Binding Property1}" Style="{StaticResource BodyTextBlockStyle}" />
</DataTemplate>

On face value this looks great – we have a simple TextBlock which is data bound and we’re using one of the out of the box styles, BodyTextBlockStyle. Unfortunately this inherits from BaseTextBlockStyle which sets the Foreground based on the current theme, as the following XAML snippet illustrates.

<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
    …….
    <Setter Property="Foreground" Value="{ThemeResource PhoneForegroundBrush}"/>
    …….
</Style>

<Style x:Key="BodyTextBlockStyle" BasedOn="{StaticResource BaseTextBlockStyle}" TargetType="TextBlock">
    <Setter Property="LineHeight" Value="24"/>
</Style>

The issue with this is that the in situ expanded view relies on Foreground being inherited from the host control, which doesn’t happen if it is explicitly set on an element (such as this case where the Foreground is being explicitly set in the TextBlock Style). The work around is simple – don’t use the built in styles, start by taking a copy of them and working with those instead eg:

<Style x:Key="NoForegroundBaseTextBlockStyle" TargetType="TextBlock">
    <Setter Property="FontFamily" Value="{ThemeResource PhoneFontFamilyNormal}"/>
    <Setter Property="FontSize" Value="{ThemeResource TextStyleLargeFontSize}"/>
    <Setter Property="TextTrimming" Value="Clip"/>
    <Setter Property="TextWrapping" Value="WrapWholeWords"/>
    <Setter Property="LineHeight" Value="21.5"/>
    <Setter Property="LineStackingStrategy" Value="BaselineToBaseline"/>
    <Setter Property="TextLineBounds" Value="Full"/>
    <Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style x:Key="NoForegroundTextBlockStyle" BasedOn="{StaticResource NoForegroundBaseTextBlockStyle}" TargetType="TextBlock">
    <Setter Property="LineHeight" Value="24" />
</Style>

When I use the NoForegroundTextBlockStyle instead of D, when I expand the ComboBox I see E (no item selected) or F (item selected) instead. Note how they pick up the foreground colour, including the accent colour for the selected item.

 

Now for the Colors

You’d have thought that adjusting the colors throughout the combobox would be relatively straight forward. Unfortunately this is not the case but let’s start with what we can adjust without tweaking the Template of the ComboBox. Selecting the ComboBox and looking at the Properties window (VS or Blend) I can adjust the Background, Foreground and BorderBrush, as shown in the following image where I’ve set each of these to a distinct color so that we can see where they appear in the ComboBox:

image

The impact on the ComboBox is as follows (see image below):

1 – In the unselected state, the Foreground color has no impact on the color of the placeholder text but the background and border colors can clearly be seen
2 & 3 – In the selected and pressed states, again, the Foreground color has no impact
4 – In the expanded state there are no color changes
5 – In the selected state, when there are 5 or fewer items, the Foreground color is evident in the text
6 – No change to the disabled state
7 – The Foreground color is used to highlight the selected item

image

Clearly the use of these three colors hasn’t been applied consistently through the ComboBox states so we’re going to have to dig deeper in order to tweak things. I’ll start by looking at the Template for the ComboBox by right-clicking on the ComboBox, selecting Edit Template, Edit a Copy.

image

There are three components of the Template being the presenter for the header, a button and the border which will house the expanding list for when there are five or fewer items.

image

The Button is what is displayed when there are 5 or more items. Selecting the Button we can see that both Background and BorderBrush are set but the Foreground is inherited. I’ll update this to be data bound using Template Binding to the Foreground button.

image

We’ve sorted out the inconsistency in 1. This actually also sorted out the foreground color in 2 and 3. However, when the button is pressed, we still see the accent color coming through, which in most cases conflicts with the branding of our app. The question is what to replace it with…. this is an issue for another day; for now, let’s just change it to use a different static resource. Right-click on the button, select Edit Template, Edit a Copy, which will take a copy of the button template and put Blend into template editing mode. From the States window, select the Pressed state.

image

In the Objects and Timeline we can see that the Background on the Border element has been set to the accent color. I’ll select the Border and from the Properties window select Convert to New Resource from the Background.

image

I’ll name the new resource ComboBoxPressedBrush and set the color to Pink so it stands out.

Solving 4 includes fixing a few things: Background, Border, Foreground and Selected Foreground. First things first, let’s change the white background to the background set on the ComboBox. Looking at the template it would appear that the background is already set using data binding through Template binding to the Background being set on the ComboBox. However, at least two of the States in the template are adjusting the background – in fact the error shown in Blend is the result of some developer not understanding how visual state groups work, or being too lazy to establish a working set of states. Anyhow, what we want to do in this case is actually to adjust both states to remove any changes to the background property.

image

In this case we need to remove the Background property from the following states:

CommonStates - Pressed
CommonStates - Disabled
CommonStates – Highlighted
DropDownStates - Opened

Expanding the ComboBox now shows the expanded view with the correct Background color. Now onto Foreground for both the unselected and selected items. The Foreground color of the selected item is actually set correctly – it makes sense for this to be the Foreground color set on the ComboBox. However, the unselected Foreground color is currently set to a theme color, ComboBoxHighlightedForegroundThemeBrush. To override this color you can simply define another brush resource with the same name eg:

<SolidColorBrush x:Key="ComboBoxHighlightedForegroundThemeBrush" Color="#FF00F3FF"/>

We do also want to address the conflicting visual states by removing the Foreground property from the DropDownStates – Opened. Also, remove the BorderBrush from the CommonStates – Highlighted state, which will fix the border on the expanded area. That solves number 4.

5 requires no changes

Addressing 6 requires two steps. The first is to overwrite some of the built in colors that control how the ComboBox appears when it is disabled. To do this we simply create new brush resources with the same name in our application. The following resources use the same Border, Background and Foreground that I set earlier on the ComboBox, except the Opacity is set to 40%

<SolidColorBrush x:Key="ComboBoxDisabledBorderThemeBrush" Color="#9900FF00" />
<SolidColorBrush x:Key="ComboBoxDisabledForegroundThemeBrush" Color="#990000FF" />
<SolidColorBrush x:Key="ComboBoxDisabledBackgroundThemeBrush" Color="#99FF0000" />

The other part is to adjust the Disabled state of the FlyoutButton. As I did earlier I needed to edit the Template for the Button and set the disabled BorderBrush, Background and Foreground to the corresponding ComboBoxDisabled brush.

7 requires a bit more exploration of what happens when the ComboBox expands to full screen in order to see the list of items. When this happens a flyout is displayed using the ListPickerFlyoutPresenter. The template for this control can be found in C:\Program Files (x86)\Windows Phone Kits\8.1\Include\abi\Xaml\Design\generic.xaml. Copying this template into the application resources means we can make changes, in this case to the Foreground and Background colors:

<SolidColorBrush x:Key="ComboBoxFullScreenExpandedBackgroundThemeBrush" Color="DarkGray" />
<SolidColorBrush x:Key="ComboBoxFullScreenExpandedForegroundThemeBrush" Color="Orange" />

<Style TargetType="controls:ListPickerFlyoutPresenter">
    <Setter Property="Foreground" Value="{StaticResource ComboBoxFullScreenExpandedForegroundThemeBrush}" />
    <Setter Property="Background" Value="{StaticResource ComboBoxFullScreenExpandedBackgroundThemeBrush}" />
    ….

The only thing this doesn’t affect is the header on the flyout page which still appears in white. To get this to use the Foreground value I just set I need to modify the FlyoutPickerTitleTextBlockStyle built in style (which is also in the generic.xaml file) by copy it into the application resources and altering the BasedOn value to use the NoForegroundBaseTextBlockStyle defined earlier.

<Style x:Key="FlyoutPickerTitleTextBlockStyle" TargetType="TextBlock" BasedOn="{StaticResource NoForegroundBaseTextBlockStyle}">
    ……
</Style>

With these changes made we’ve got much better control over how the ComboBox displays text and how the border, background and foreground colors are applied.

image

 

Hopefully in this post you’ve seen how you can jump in and alter colors throughout the ComboBox templates. There are a series of built in colors both in generic.xaml and themeresources.xaml (C:\Program Files (x86)\Windows Phone Kits\8.1\Include\abi\Xaml\Design) which can be overwritten to do simple changes but sometimes you need to modify the underlying templates.

Comments (3) -

  • Fernando

    8/24/2014 6:20:27 AM |

    OMG. That is awesome

  • Martin Anderson

    8/25/2014 9:19:58 PM |

    I have often felt that these built in controls - rather than just making sure every colour/thickness/etc value is a resource, that the properties should be bound to public dependency properties - to allow designers to set the colours for every element and state.

    But I dare say there would be performance issues from the extra bindings which can be mitigated by the way they currently do it, and still allowing those resources to be overwritten in the app.xaml

  • Joerg

    8/26/2014 6:51:24 AM |

    Nick,
    thank you very much for this great blog post. This helped me a lot solving some issues I had with the combo box! Also, I do now know how to edit styles!

    Kind regards

Pingbacks and trackbacks (2)+

Comments are closed
Limitations of the WebView in Windows 8 Metro Apps

Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Limitations of the WebView in Windows 8 Metro Apps

Having worked quite intimately with the WebBrowser control in Windows Phone I was a little flawed by how immature and annoying the WebView for Windows 8 metro app is. As noted by Brent Schooley in his post on Metro WebView Source and HTML workarounds you can work around some of the limitations such as the missing SaveToString method. One of the features of the WebBrowser control that I quite often use is the series of events that are raised around navigating to pages within the control. Unlike the WebBrowser, the WebView is missing events such as Navigating, Navigated and NavigationFailed, and has only the LoadCompleted event. I figured that I should be able to detect when the current page is changing, and thus raise a Navigating event. Turns out that it is possible with a combination of InvokeScript (to invoke some javascript within the page) and window.external.notify (to raise an event when the page in the browser is changing). The following WebViewWrapper class exposes a Navigating event which you can wire an event handler to in order to detect when the browser is being navigated away from the current page (unfortunately there doesn’t seem to be a way to extract the target url, nor to be able to cancel the navigation, unlike the real Navigating event that the WebBrowser control has). I’ve also included a SaveAsString method which wraps the code Brent had in his post.

public class WebViewWrapper
{
    // Maintain a reference to the WebView control so that
    // we can invoke javascript
    public WebView WebView { get; private set; }

    public WebViewWrapper(WebView webView)
    {
        WebView = webView;
    }

    // The Navigating event is a custom event, allowing us to hook/unhook
    // from the ScriptNotify and LoadCompleted events. To invoke this
    // event, we actually invoke the internalNavigating event.
    private event EventHandler<NavigatingEventArgs> internalNavigating;
    public event EventHandler<NavigatingEventArgs> Navigating
    {
        add
        {
            WebView.ScriptNotify+=NavigatingScriptNotify;
            WebView.LoadCompleted+=WireUpNavigating;
            internalNavigating += value;
        }
        remove
        {
            WebView.ScriptNotify -= NavigatingScriptNotify;
            WebView.LoadCompleted-=WireUpNavigating;
            internalNavigating -= value;
        }
    }

    // When each page loads, run a javascript function which wires up
    // an event handler to the onbeforeunload event on the window.  This
    // event is raised when the window is about to unload the current page
    // In the event handler we call window.external.notify in order to raise
    // the ScriptNotify event on the WebView. The javascript function also
    // returns the current document location. This is used to update the
    // AllowedScriptNotifyUris property on the WebView in order to permit
    // the current document to call window.external.notify (remembering
    // that even though we injected the javascript, it’s being invoked in the
    // context of the current document.
    private void WireUpNavigating(object sender, NavigationEventArgs e)
    {
        var unloadFunc = "(function(){" +
                            " function navigating(){" +
                            "  window.external.notify('%%' + location.href);" +
                            "} " +
                            "window.onbeforeunload=navigating;" +
                            "return location.href;" +
                            "})();";
        var host = WebView.InvokeScript("eval", new[] { unloadFunc });
        WebView.AllowedScriptNotifyUris = new[] { new Uri(host) };
    }

   // Check to see if the ScriptNotify was raised by the javascript we
   // injected (ie does it start with %%), and then raise the Navigating
   // event.
   private void NavigatingScriptNotify(object sender, NotifyEventArgs e)
    {
        if (internalNavigating == null) return;
        if(!string.IsNullOrWhiteSpace(e.Value))
        {
            if(e.Value.StartsWith("%%"))
            {
                internalNavigating(this, new NavigatingEventArgs() {LeavingUri = new Uri(e.Value.Trim('%'))});
            }
        }
    }

    public string SaveToString()
    {
        try
        {
            var retrieveHtml = "document.documentElement.outerHTML;";
            var html = WebView.InvokeScript("eval", new[] { retrieveHtml });
            return html;
        }
        catch
        {
            return null;
        }
    }
}

public class NavigatingEventArgs:EventArgs
{
    public Uri LeavingUri { get; set; }
}

Actually making use of the WebViewWrapper class is as simple as creating an instance and then wiring up an event handler.

public BlankPage()
{
    this.InitializeComponent(); 

    var wrapper = new WebViewWrapper(MyWebView);
    wrapper.Navigating += WrapperNavigating;
}

void WrapperNavigating(object sender, NavigatingEventArgs e)
{
    Debug.WriteLine(e.LeavingUri);
    Debug.WriteLine((sender as WebViewWrapper).SaveToString());
}

Comments are closed