wp7 Keyboard helper as a behavior

Thanks to a number of comments regarding the problems I had with the ApplicationBar and the keyboard I’ve decided to apply some of the ideas to my previous post (thanks everyone for the comments) about a keyboard helper to create a self contained keyboard tab helper behavior (mouthful). I’ve posted the full code and binaries, Wp7KeyboardSource.zip
but essentially all you now have to do is;

<Grid x:Name=ContentGrid Grid.Row=1>

  <i:Interaction.Behaviors>

    <keyboard:KeyboardTabHelperBehavior />

  </i:Interaction.Behaviors>

  <StackPanel Orientation=Vertical>

    <CheckBox IsTabStop=True TabIndex=4 Content=Some check/>

    <TextBox TabIndex=1 Text=start here/>

    <TextBox IsTabStop=True Text=Won’t tab here cause not set tabindex/>

    <TextBox IsEnabled=False TabIndex=2 Text=not here cause disabled/>

    <TextBox TabIndex=3 Text=next stop here/>

  </StackPanel>

</Grid>

 …and that’s it, no code behind, no adding of event handlers. Just set up your tab index and stops and it will take care of the rest.

Advertisements

Wp7 vs keyboard vs ApplicationBar vs binding

As you may have read in my previous posts I’m currently trying to iron out some nagging issues with Windows Phone 7 keyboard (SIP). Another problem I’ve hit is when using it in combination with binding and the ApplicationBar. My scenario is straight forward, I have a TextBox for the user’s name which, via binding, changes a view model’s representation of the name (and vice versa). I then have what amounts to a save button in the Application Bar (application bar icon button). When I press the save button I want; a) the keyboard to disappear b) the view model updated with the current text entered for the user name. Sounds simple, and yet it doesn’t work. Making the keyboard disappear is easy enough, I’ve extended the Keyboard helper class to have a HideKeyboard method – which simply does page.Focus(). So what’s the problem? When the save button is pressed it;

  1. calls HideKeyboard which sets the focus to the page
  2. which in turn causes the text box to lose focus and fire the OnPropertyChanged for data binding
  3. calls my SaveName service.

Again seems simple, but the problem is the timing of the events. What actually happens is the execution order is 1->3->2. Why? Well the binding events are only executed once control returns from the function that caused the focus to change. Therefore my service is trying to save the name before the name has been updated, argh! The simplest solution I can think of (please say if you have a better method) is this hack;

void Default_Loaded(object sender, RoutedEventArgs e)
{

    this.keyboardHelper = new KeyboardHelper(this, LayoutRoot);
    this.keyboardHelper.AddKeyUpHander(typeof(TextBox), TabControls_KeyUp);
    this.DataContext = viewModel;
    this.viewModel.PropertyChanged +=
        new PropertyChangedEventHandler(viewModel_PropertyChanged);
}


void saveButton_Click(object sender, EventArgs e)
{
    // hide the keyboard
    this.keyboardHelper.HideKeyboard();
    // [Edit] If you edit one control and then tab to the next this won't work
    // because the 1st control fires the property change but we've not yet pressed saved
    // so I now have to push the values into the view model, i.e. lose some of the clean adv of binding
    this.viewModel.Name = TextBoxName.Text;

    // [Edit 2] Please see comments; 'SS' suggested queing the save via the dispatcher, which is much nicer
    // I'm not sure if you can guarantee the binding will occur first but it's much nicer
    //  Dispatcher.BeginInvoke(() => viewModel.VerifyProfile());
    // remember that we actually want to verify this change
    this.doesWantToVerify = true;
}

private bool doesWantToVerify;
void viewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // some view model property has changed but 
    // did we really want to verify as well?
    if (doesWantToVerify)
    {
        doesWantToVerify = false;
        viewModel.VerifyProfile();
    }
}

Keyboard helper class for wp7 vs tab index

I’ve recently blogged on tumblr about a way to plug the missing tab-to-next-control functionaly in wp7 when using the software keyboard (SIP). I’ve reafactored the code to make it a little easier to use, plus added a little bug fix concerning isEnabled=false controls.

<!--ContentPanel - place additional content here-->

<Grid x:Name="ContentGrid" Grid.Row="1">

  <StackPanel Orientation="Vertical">

    <CheckBox IsTabStop="True" TabIndex="4" Content="Some check"></CheckBox>

    <TextBox TabIndex="1" Text="start here" KeyUp="TextBox_KeyUp"></TextBox>

    <TextBox IsTabStop="True" Text="Won't tab here cause not set tabindex"></TextBox>

    <TextBox IsEnabled="False" TabIndex="2" Text="not here cause disabled" KeyUp="TextBox_KeyUp"></TextBox>

    <TextBox TabIndex="3" Text="next stop here" KeyUp="TextBox_KeyUp"></TextBox>

  </StackPanel>

</Grid>


A little bit of code-behind is needed;

public partial class MainPage : PhoneApplicationPage

{

    private KeyboardHelper keyboardHelper;

 

    public MainPage()

    {

        InitializeComponent();

        this.Loaded += new RoutedEventHandler(MainPage_Loaded);

        this.IsTabStop = true;

    }

 

    void MainPage_Loaded(object sender, RoutedEventArgs e)

    {

        this.keyboardHelper = new KeyboardHelper(this, LayoutRoot);

    }

 

    private void TextBox_KeyUp(object sender, KeyEventArgs e)

    {

        if (e.Key == Key.Enter)

        {

            keyboardHelper.HandleReturnKey(sender);

        }

    }

}

…and the supporting helper class;

// Wp7Keyboard helper by @pauliom

using System;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

using System.Collections.Generic;

using Microsoft.Phone.Controls;

 

namespace Wp7Keyboard

{

    /// <summary>

    /// Helper to provide missing tab-to-next-control functionality

    /// </summary>

    public class KeyboardHelper

    {

        private List<Control> tabbedControls;

        private Control lastTabbedControl = null;

        private Panel layoutRoot;

        private PhoneApplicationPage page;

 

        /// <summary>

        /// Constructor to support a phone page

        /// </summary>

        /// <param name="page"></param>

        /// <param name="layoutRoot"></param>

        public KeyboardHelper(PhoneApplicationPage page, Panel layoutRoot)

        {

            this.layoutRoot = layoutRoot;

            this.page = page;

            RefeshTabbedControls(layoutRoot);

        }

 

        /// <summary>

        /// Refresh the tabbed controls collection, helpful if you dynamically alter the controls

        /// </summary>

        /// <param name="layoutRoot"></param>

        public void RefeshTabbedControls(Panel layoutRoot)

        {

            this.tabbedControls = GetChildsRecursive(layoutRoot).OfType<Control>().Where(c => c.IsTabStop && c.TabIndex != int.MaxValue).OrderBy(c => c.TabIndex).ToList();

            if (this.tabbedControls != null && this.tabbedControls.Count > 0)

            {

                this.lastTabbedControl = this.tabbedControls[this.tabbedControls.Count - 1];

            }

        }

 

        // code from 'tucod'

        IEnumerable<DependencyObject> GetChildsRecursive(DependencyObject root)

        {

            List<DependencyObject> elts = new List<DependencyObject>();

            elts.Add(root);

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)

                elts.AddRange(GetChildsRecursive(VisualTreeHelper.GetChild(root, i)));

 

            return elts;

        }

 

        /// <summary>

        /// Process a return key from the client controls key-up event

        /// </summary>

        /// <param name="sender"></param>

        internal void HandleReturnKey(object sender)

        {

            Control eventSender = sender as Control;

            if (eventSender != null)

            {

                Control thisControlInTabbedList = tabbedControls.FirstOrDefault(c => c == eventSender);

                if (thisControlInTabbedList != null)

                {

                    // what's the next control?                                                                       

                    SetFocusOnNextControl(thisControlInTabbedList);

                }

            }

        }

 

        private void SetFocusOnNextControl(Control thisControlInTabbedList)

        {

            if (lastTabbedControl == thisControlInTabbedList)

            {

                // we've come the end so remove the keyboard

                this.page.Focus();

            }

            else

            {

                Control nextControl = tabbedControls.FirstOrDefault(c => c.TabIndex > thisControlInTabbedList.TabIndex);

                bool wasFocusSet = false;

                if (nextControl != null)

                {

                    wasFocusSet = nextControl.Focus();

                }

 

                if (!wasFocusSet)

                {

                    SetFocusOnNextControl(nextControl);

                }

 

            }

        }

    }

}

Hope you find it useful, again maybe it won’t be needed after RTM, finders crossed.
BTW I’m trying out new blog engines hence the scatter of links.