Blograby

XAML Editor (Dynamic XAML & Popup)

XAML Editor is a text editor for XAML, much like the famous XAMLPad program for the Windows desktop. At first, XAML Editor looks like nothing more than a page with a text box, but it is much more for a number of reasons:

The custom text suggestions bar is vital for making this app usable, as without it common XAML characters like angle brackets, the forward slash, quotes, and curly braces are buried in inconvenient locations. With this bar, users don’t normally need to leave the first page of keyboard keys unless they are typing numbers.

On the surface, the main lesson for this chapter seems like the mechanism for reading XAML at run-time and producing a dynamic user interface. However, this is accomplished with just one line of code. The main challenge to implementing XAML Editor is providing a custom text suggestions bar. The real text suggestions bar does not support customization, so XAML Editor provides one with a lot of trickery involving an element known as Popup.

The trickery (or, to be honest, hacks) done by this chapter also forms a cautionary tale. In the initial version of Windows Phone 7 (version 7.0.7004.0), the fake suggestions bar was a reasonable replacement for the built-in one, as shown in Figure 11.1. With the addition of the copy/paste feature (starting with version 7.0.7338.0), however, it can no longer act this way. The app had relied on suppressing the real bar by using the default input scope on the text box, but app authors can no longer reliably do this because the bar still appears whenever something can be pasted. Furthermore, there is no way for the fake bar to integrate with the clipboard and provide its own paste button. Therefore, the latest version of XAML Editor treats the fake suggestions bar as a second bar on top of the primary one.

FIGURE 11.1 Because the Windows Phone copy/paste feature did not yet exist, the first version of XAML Editor could reliably place the fake suggestions bar where the real one would be.

Popup

A popup is an element that floats on top of other elements. It was designed for temporary pieces of UI, such as tooltips. However, as in this chapter, is often used in hacky ways to produce behavior that is difficult to accomplish otherwise.

A popup doesn’t have any visual appearance by itself, but it can contain a visual element as its single child (and that child could be a complex element containing other elements). By default, a popup docks to the top-left corner of its parent, although you can move it by giving it a margin and/or setting its HorizontalOffset and VerticalOffset properties.

On Top of (Almost) Everything

Figure 11.2 demonstrates the behavior of the popup in the following page:

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
SupportedOrientations=”PortraitOrLandscape” Orientation=”Landscape”>
<Grid>
<!– Inner grid with a button-in-popup and a separate button –>
<Grid Background=”Red” Margin=”100”>
<Popup IsOpen=”True”>
<Button Content=”button in popup in grid” Background=”Blue”/>
</Popup>
<Button Content=”button in grid” Canvas.ZIndex=”100”/>
</Grid>
<!– A rectangle that overlaps the inner grid underneath it –>
<Rectangle Width=”200” Height=”200” Fill=”Lime”
HorizontalAlignment=”Left” VerticalAlignment=”Top”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

FIGURE 11.2 The popup’s content doesn’t stretch, stays in the top-left corner of its parent, and stays on top of all other elements.

There are three interesting things to note about Figure 11.2:

Exempt from Orientation Changes

Besides their topmost rendering, popups have another claim to fame: they are able to ignore orientation changes! This happens when you create and show a popup without attaching it to any parent element. In this case, it is implicitly attached to the root frame, which always acts as if it is in the portrait orientation.

The following empty page demonstrates this behavior:

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
SupportedOrientations=”PortraitOrLandscape” Orientation=”Landscape”>
<Grid x:Name=”Grid”/>
</phone:PhoneApplicationPage>

[/code]

In this page’s code-behind, two popups are created in the constructor. The first one is attached to the grid, but the second one is implicitly attached to the frame:

[code]

public MainPage()
{
InitializeComponent();
Popup popup1 = new Popup();
popup1.Child = new Button { Content = “button in popup in grid”, FontSize=40 };
popup1.IsOpen = true;
this.Grid.Children.Add(popup1); // Attach this to the grid
Popup popup2 = new Popup();
popup2.Child = new Button { Content = “button in popup”, FontSize=55,
Foreground = new SolidColorBrush(Colors.Cyan),
BorderBrush = new SolidColorBrush(Colors.Cyan) };
popup2.IsOpen = true; // Show without explicitly attaching it to anything
}

[/code]

This page is shown in Figure 11.3. The cyan button (inside popup2) behaves like the whole screen would behave if it were marked as SupportedOrientations=”Portrait”, whereas the white button (inside popup1) adjusts to remain on the edges of the screen currently acting as the top and the left.

FIGURE 11.3 The popup that isn’t attached to the grid stays docked to the physical top and left of the phone for any orientation.

Frame-rooted popups also do not move with the rest of the page when the on-screen keyboard automatically pushes the page upward to keep the focused textbox visible. XAML Editor leverages this fact, as the popup containing the text suggestions bar must always be in the exact same spot regardless of what has happened to the page.

The User Interface

Listing 11.1 contains the XAML for this app’s only page, shown at the beginning of this chapter. The page contains a text box on top of a grid used to hold the rendered result from parsing the XAML, and an application bar with four buttons and four menu items.

LISTING 11.1 MainPage.xaml—The User Interface for XAML Editor

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
Loaded=”MainPage_Loaded”
SupportedOrientations=”PortraitOrLandscape”>
<!– Application bar with 3-4 buttons and 4 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”view” Click=”SwitchViewButton_Click”
IconUri=”/Shared/Images/appbar.view.png”/>
<shell:ApplicationBarIconButton Text=”clear” Click=”ClearButton_Click”
IconUri=”/Shared/Images/appbar.cancel.png”/>
<shell:ApplicationBarIconButton Text=”email” Click=”EmailButton_Click”
IconUri=”/Shared/Images/appbar.email.png”/>
<shell:ApplicationBarIconButton Text=”error” Click=”ErrorButton_Click”
IconUri=”/Shared/Images/appbar.error.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”simple shapes”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”gradient text”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”clipped image”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”controls”
Click=”SampleMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– 1×1 grid containing 2 overlapping child grids –>
<Grid>
<!– where the live XAML goes –>
<Grid x:Name=”ViewPanel”/>
<!– The text editor–>
<Grid x:Name=”EditorPanel” Background=”{StaticResource PhoneBackgroundBrush}”
Opacity=”.9”>
<ScrollViewer x:Name=”ScrollViewer”>
<TextBox x:Name=”XamlTextBox” AcceptsReturn=”True” VerticalAlignment=”Top”
Height=”2048” TextWrapping=”Wrap” InputScope=”Text”
FontFamily=”Courier New” FontSize=”19” FontWeight=”Bold”
SelectionChanged=”XamlTextBox_SelectionChanged”
GotFocus=”XamlTextBox_GotFocus” LostFocus=”XamlTextBox_LostFocus”
TextChanged=”XamlTextBox_TextChanged”/>
</ScrollViewer>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

Elements have a size limitation!

You should avoid making any Silverlight element larger than 2,048 pixels in any dimension, due to system limitations.Otherwise, a variety of behaviors can be seen, such as forced clipping or even the entire screen going blank! The best workaround for a text box would be to virtualize its contents, e.g. only make it contain the on-screen contents (and perhaps a little more) at any single time. Implementing such a scheme while making sure scrolling and typing works as expected can be complex. XAML Editor simply hopes that users don’t type more than approximately 93 lines of XAML!

The Code-Behind

Listing 11.2 contains the code-behind for the main page.

LISTING 11.2 MainPage.xaml.cs—The Code-Behind for XAML Editor

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Always remember the text box’s text, caret position and selection
Setting<string> savedXaml = new Setting<string>(“XAML”, Data.SimpleShapes);
Setting<int> savedSelectionStart = new Setting<int>(“SelectionStart”, 0);
Setting<int> savedSelectionLength = new Setting<int>(“SelectionLength”, 0);
// The popup and its content are not attached to the page
internal Popup Popup;
internal TextSuggestionsBar TextSuggestionsBar;
// Named fields for two application bar buttons
IApplicationBarIconButton viewButton;
IApplicationBarIconButton errorButton;
// Remember the current XAML parsing error in case the user wants to see it
string currentError;
// A timer for delaying the update of the view after keystrokes
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
public MainPage()
{
InitializeComponent();
// Assign the application bar buttons because they can’t be named in XAML
this.viewButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
this.errorButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
// Initialize the popup and its content
this.TextSuggestionsBar = new TextSuggestionsBar(this.XamlTextBox);
this.Popup = new Popup();
this.Popup.Child = this.TextSuggestionsBar;
// PopupHelper does the dirty work of positioning & rotating the popup
PopupHelper.Initialize(this);
// When the timer ticks, refresh the view then stop it, so there’s
// only one refresh per timer.Start()
this.timer.Tick += delegate(object sender, EventArgs e)
{
RefreshView();
this.timer.Stop();
};
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember the text box’s text, caret position and selection
this.savedXaml.Value = this.XamlTextBox.Text;
this.savedSelectionStart.Value = this.XamlTextBox.SelectionStart;
this.savedSelectionLength.Value = this.XamlTextBox.SelectionLength;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the text box’s text, caret position and selection
this.XamlTextBox.Text = this.savedXaml.Value;
this.XamlTextBox.SelectionStart = this.savedSelectionStart.Value;
this.XamlTextBox.SelectionLength = this.savedSelectionLength.Value;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// Make on-screen keyboard instantly appear
this.XamlTextBox.Focus();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Send mouse info to the text suggestions bar, if appropriate
if (PopupHelper.IsOnPopup(e))
this.TextSuggestionsBar.OnMouseDown(PopupHelper.GetPopupRelativePoint(e));
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Send mouse info to the text suggestions bar, if appropriate
if (PopupHelper.IsOnPopup(e))
this.TextSuggestionsBar.OnMouseMove(PopupHelper.GetPopupRelativePoint(e));
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Send mouse info to the text suggestions bar, in case its appropriate
this.TextSuggestionsBar.OnMouseUp(PopupHelper.IsOnPopup(e));
}
void XamlTextBox_GotFocus(object sender, RoutedEventArgs e)
{
// Show the popup whenever the text box has focus (and is visible)
if (this.EditorPanel.Visibility == Visibility.Visible)
this.Popup.IsOpen = true;
}
void XamlTextBox_LostFocus(object sender, RoutedEventArgs e)
{
// Hide the popup whenever the text box loses focus
this.Popup.IsOpen = false;
}
void XamlTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
// Update the suggestions based on the text behind the caret location
string text = this.XamlTextBox.Text;
int position = this.XamlTextBox.SelectionStart – 1;
// Initiate the suggestion-picking algorithm on a background thread
BackgroundWorker backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += delegate(object s, DoWorkEventArgs args)
{
// This runs on a background thread
args.Result = UpdateTextSuggestions(text, position);
};
backgroundWorker.RunWorkerCompleted +=
delegate(object s, RunWorkerCompletedEventArgs args)
{
// This runs on the UI thread after BackgroundWorker_DoWork is done
// Grab the list created on the background thread
IList<Suggestion> suggestions = args.Result as IList<Suggestion>;
if (suggestions == null)
return;
// Clear the current list
this.TextSuggestionsBar.ClearSuggestions();
// Fill the bar with the new list
foreach (Suggestion suggestion in suggestions)
this.TextSuggestionsBar.AddSuggestion(suggestion);
};
backgroundWorker.RunWorkerAsync();
}
void XamlTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Remember the current caret position and selection
int start = this.XamlTextBox.SelectionStart;
int length = this.XamlTextBox.SelectionLength;
// Ensure the text always ends with several newlines so the user
// can easily scroll to see the very bottom of the text
if (!this.XamlTextBox.Text.EndsWith(Constants.NEWLINES))
this.XamlTextBox.Text = this.XamlTextBox.Text.TrimEnd()
+ Constants.NEWLINES;
// Restore the caret position and selection, which gets
// overwritten if the text is updated
this.XamlTextBox.SelectionStart = start;
this.XamlTextBox.SelectionLength = length;
// Cancel any pending refresh
if (this.timer.IsEnabled)
this.timer.Stop();
// Schedule a refresh of the view for one second from now
this.timer.Start();
}
void RefreshView()
{
try
{
// Wrap the user’s text in a page with appropriate namespace definitions
string xaml = @”<phone:PhoneApplicationPage
xmlns=””http://schemas.microsoft.com/winfx/2006/xaml/presentation””
xmlns:x=””http://schemas.microsoft.com/winfx/2006/xaml””
xmlns:phone=””clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone””
FontFamily=””{StaticResource PhoneFontFamilyNormal}””
FontSize=””{StaticResource PhoneFontSizeNormal}””
Foreground=””{StaticResource PhoneForegroundBrush}””>”
+ this.XamlTextBox.Text
+ “</phone:PhoneApplicationPage>”;
// Parse the XAML and get the root element (the page)
UIElement root = System.Windows.Markup.XamlReader.Load(xaml) as UIElement;
// Replace ViewPanel’s content with the new elements
this.ViewPanel.Children.Clear();
this.ViewPanel.Children.Add(root);
// An exception wasn’t thrown, so clear any error state
this.XamlTextBox.Foreground = new SolidColorBrush(Colors.Black);
this.ApplicationBar.Buttons.Remove(this.errorButton);
}
catch (Exception ex)
{
// The XAML was invalid, so transition to an error state
this.XamlTextBox.Foreground = new SolidColorBrush(Colors.Red);
if (!this.ApplicationBar.Buttons.Contains(this.errorButton))
this.ApplicationBar.Buttons.Add(this.errorButton);
// Use the exception message as the error message, but remove the line #
this.currentError = ex.Message;
if (this.currentError.Contains(“ [Line:”))
this.currentError = this.currentError.Substring(0,
this.currentError.IndexOf(“ [Line:”));
}
}
IList<Suggestion> UpdateTextSuggestions(string text, int position)
{
// The list of suggestions to report
List<Suggestion> suggestions = new List<Suggestion>();
if (position == -1)
{
// We’re at the beginning of the text box
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
return suggestions;
}
char character = text[position];
if (Char.IsDigit(character))
{
// A number is likely a value to be followed by an end quote, or it could
// be a property like X1 or X2 to be followed by an equals sign
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “=”, InsertionOffset = 0 });
}
else if (!Char.IsLetter(character))
{
// Choose various likely completions based on the special character
switch (character)
{
case ‘<’:
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
break;
case ‘/’:
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
break;
case ‘ ‘:
case ‘r’:
case ‘n’:
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
break;
case ‘>’:
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
break;
case ‘=’:
case ‘}’:
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
break;
case ‘{‘:
suggestions.Add(
new Suggestion { Text = “Binding “, InsertionOffset = 0 });
suggestions.Add(
new Suggestion { Text = “StaticResource “, InsertionOffset = 0 });
break;
case ‘“‘:
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “{“, InsertionOffset = 0 });
break;
}
}
else
{
// This is a letter
// First add a few special symbols
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “=”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “}”, InsertionOffset = 0 });
// Keep traversing backwards until we hit a non-letter
string letters = null;
while (position >= 0 && (letters == null ||
Char.IsLetter(text[position])))
letters = text[position–] + letters;
// Add words from our custom dictionary that match the current text as
// as prefix
for (int i = 0; i < Data.Words.Length; i++)
{
// Only include exact matches if the case is different
// (so the user can tap the suggestion to fix their casing)
if (Data.Words[i].StartsWith(letters,
StringComparison.InvariantCultureIgnoreCase) &&
!Data.Words[i].Equals(letters, StringComparison.InvariantCulture))
{
suggestions.Add(new Suggestion { Text = Data.Words[i],
InsertionOffset = -letters.Length });
}
}
}
return suggestions;
}
// Application bar handlers
void ViewButton_Click(object sender, EventArgs e)
{
// Switch between viewing the results and viewing the XAML text box
if (this.EditorPanel.Visibility == Visibility.Visible)
{
this.EditorPanel.Visibility = Visibility.Collapsed;
this.viewButton.IconUri = new Uri(“/Images/appbar.xaml.png”,
UriKind.Relative);
this.viewButton.Text = “xaml”;
}
else
{
this.EditorPanel.Visibility = Visibility.Visible;
this.viewButton.IconUri = new Uri(“/Shared/Images/appbar.view.png”,
UriKind.Relative);
this.viewButton.Text = “view”;
this.XamlTextBox.Focus();
}
}
void ClearButton_Click(object sender, EventArgs e)
{
// Clear the text box if the user agrees
if (MessageBox.Show(“Are you sure you want to clear this XAML?”,
“Clear XAML”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
this.XamlTextBox.Text = “”;
}
void EmailButton_Click(object sender, EventArgs e)
{
// Launch an email with the content of the text box
EmailComposeTask emailLauncher = new EmailComposeTask {
Body = this.XamlTextBox.Text, Subject = “XAML from the XAML Editor app” };
emailLauncher.Show();
}
void ErrorButton_Click(object sender, EventArgs e)
{
// Show whatever the current error is
MessageBox.Show(this.currentError, “XAML Error”, MessageBoxButton.OK);
}
void SampleMenuItem_Click(object sender, EventArgs e)
{
if (this.XamlTextBox.Text.Trim().Length != 0 &&
MessageBox.Show(“Are you sure you want to replace the XAML?”,
“Replace XAML”, MessageBoxButton.OKCancel) != MessageBoxResult.OK)
return;
// Fill the text box with the chosen sample
switch ((sender as IApplicationBarMenuItem).Text)
{
case “simple shapes”:
this.XamlTextBox.Text = Data.SimpleShapes;
break;
case “gradient text”:
this.XamlTextBox.Text = Data.GradientText;
break;
case “clipped image”:
this.XamlTextBox.Text = Data.ClippedImage;
break;
case “controls”:
this.XamlTextBox.Text = Data.Controls;
break;
}
}
}
}

[/code]

Notes:

PopupHelper

Listing 11.3 contains the implementation of the PopupHelper class used by Listing 11.2. It is directly tied to the main page rather than being any sort of reusable control.

LISTING 11.3 PopupHelper.cs—A Class That Manipulates the Popup Containing the Text Suggestions Bar

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
internal static class PopupHelper
{
static MainPage page;
static bool textSuggestionsBarSlidDown;
internal static void Initialize(MainPage p)
{
page = p;
page.OrientationChanged += Page_OrientationChanged;
page.TextSuggestionsBar.DownButtonTap += TextSuggestionsBar_DownButtonTap;
AdjustForCurrentOrientation();
}
// Report whether the mouse event occurred within the popup’s bounds
internal static bool IsOnPopup(MouseEventArgs e)
{
if (!page.Popup.IsOpen)
return false;
Point popupRelativePoint = GetPopupRelativePoint(e);
return (popupRelativePoint.Y >= 0 &&
popupRelativePoint.Y < page.TextSuggestionsBar.ActualHeight);
}
// Return the X,Y position of the mouse, relative to the popup
internal static Point GetPopupRelativePoint(MouseEventArgs e)
{
Point popupRelativePoint = new Point();
// We can use the page-relative X as the popup-relative X
Point pageRelativePoint = e.GetPosition(page);
popupRelativePoint.X = pageRelativePoint.X;
// We can’t use the page-relative Y because the page can be automatically
// “pushed” by the on-screen keyboard, whereas the floating popup remains
// still. Therefore, first get the frame-relative Y:
Point frameRelativePoint = e.GetPosition(null /* the frame */);
popupRelativePoint.Y = frameRelativePoint.Y;
// A frame-relative point is always portrait-oriented, so invert
// the value if we’re currently in a landscape orientation
if (IsMatchingOrientation(PageOrientation.Landscape))
popupRelativePoint.Y = frameRelativePoint.X;
// Now adjust the Y to be relative to the top of the popup
// rather than the top of the screen
if (IsMatchingOrientation(PageOrientation.LandscapeLeft))
popupRelativePoint.Y = -(popupRelativePoint.Y+page.Popup.VerticalOffset);
else
popupRelativePoint.Y -= page.Popup.VerticalOffset;
return popupRelativePoint;
}
static void Page_OrientationChanged(object sender,
OrientationChangedEventArgs e)
{
// Clear the slid-down setting on any orientation change
textSuggestionsBarSlidDown = false;
AdjustForCurrentOrientation();
}
static void TextSuggestionsBar_DownButtonTap(object sender, EventArgs e)
{
textSuggestionsBarSlidDown = true;
AdjustForCurrentOrientation();
}
static bool IsMatchingOrientation(PageOrientation orientation)
{
return ((page.Orientation & orientation) == orientation);
}
static void AdjustForCurrentOrientation()
{
page.TextSuggestionsBar.ResetScrollPosition();
if (IsMatchingOrientation(PageOrientation.Portrait))
{
// Adjust the position, size, and rotation for portrait
page.TextSuggestionsBar.MinWidth = Constants.SCREEN_WIDTH;
page.Popup.HorizontalOffset = 0;
page.Popup.VerticalOffset = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS – Constants.PORTRAIT_KEYBOARD_HEIGHT
– Constants.TEXT_SUGGESTIONS_HEIGHT*2; // 1 for the real bar, 1 for this
page.Popup.RenderTransform = new RotateTransform { Angle = 0 };
if (textSuggestionsBarSlidDown)
page.Popup.VerticalOffset += Constants.PORTRAIT_KEYBOARD_HEIGHT;
}
else
{
// Adjust the position, size, and rotation for landscape
page.TextSuggestionsBar.MinWidth = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS;
if (IsMatchingOrientation(PageOrientation.LandscapeLeft))
{
page.Popup.RenderTransform = new RotateTransform { Angle = 90 };
page.Popup.HorizontalOffset = 0;
page.Popup.VerticalOffset = -(Constants.LANDSCAPE_KEYBOARD_HEIGHT +
Constants.TEXT_SUGGESTIONS_HEIGHT*2);
// 1 for the real bar, 1 for this
}
else // LandscapeRight
{
page.Popup.RenderTransform = new RotateTransform { Angle = 270 };
page.Popup.Width = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS;
page.Popup.HorizontalOffset = -page.Popup.Width;
page.Popup.VerticalOffset = Constants.SCREEN_WIDTH –
Constants.LANDSCAPE_KEYBOARD_HEIGHT –
Constants.TEXT_SUGGESTIONS_HEIGHT*2;
// 1 for the real bar, 1 for this
}
if (textSuggestionsBarSlidDown)
page.Popup.VerticalOffset += Constants.LANDSCAPE_KEYBOARD_HEIGHT;
}
}
}
}

[/code]

The TextSuggestionsBar User Control

The TextSuggestionsBar user control handles the display of the dot-delimited text suggestions and the proper tapping and scrolling interaction. It also contains a workaround for a problem with hardware keyboards.

Ideally, the popup containing this control would automatically position itself above the on-screen keyboard when it is used, but close to the bottom edge of the screen when a hardware keyboard is used instead. Unfortunately, there is no good way to detect when a hardware keyboard is in use, so this app relies on the user to move it. The TextSuggestionsBar has an extra “down” button that is hidden under the on-screen keyboard when it is in use, but revealed when a hardware keyboard is used. The user can tap this button to move the bar to the bottom, just above the real text suggestions bar. Figure 11.4 shows what this looks like. Rather than consuming space with a corresponding “up” button, this app only moves the bar back to its higher position when the phone’s orientation changes.

FIGURE 11.4 The user must manually move the custom text suggestions bar to the appropriate spot when using a hardware keyboard.

Listing 11.4 contains the XAML for this user control, and Listing 11.5 contains the codebehind.

LISTING 11.4 TextSuggestionsBar.xaml—The User Interface for the TextSuggestionsBar User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.TextSuggestionsBar”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
IsHitTestVisible=”False”>
<StackPanel>
<Canvas Background=”{StaticResource PhoneChromeBrush}” Height=”62”>
<!– The suggestions go in this stack panel –>
<StackPanel x:Name=”StackPanel” Orientation=”Horizontal” Height=”62”/>
</Canvas>
<!– The double-arrow “button” (just a border with a path) –>
<Border Background=”{StaticResource PhoneChromeBrush}” Width=”62”
Height=”62” HorizontalAlignment=”Left”>
<Path Fill=”{StaticResource PhoneForegroundBrush}”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
Data=”M0,2 14,2 7,11z M0,13 14,13 7,22”/>
</Border>
</StackPanel>
</UserControl>

[/code]

LISTING 11.5 TextSuggestionsBar.xaml.cs—The Code-Behind for the TextSuggestionsBar User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public partial class TextSuggestionsBar : UserControl
{
// A custom event, raised when the down button is tapped
public event EventHandler DownButtonTap;
TextBox textBox;
double mouseDownX;
double mouseMoveX;
Border pressedSuggestionElement;
int selectionStart;
int selectionLength;
public TextSuggestionsBar(TextBox textBox)
{
InitializeComponent();
this.textBox = textBox;
}
public void OnMouseDown(Point point)
{
// Grab the current position/selection before it changes! The text box
// still has focus, so the tap is likely to change the caret position
this.selectionStart = this.textBox.SelectionStart;
this.selectionLength = this.textBox.SelectionLength;
this.mouseDownX = this.mouseMoveX = point.X;
this.pressedSuggestionElement = FindSuggestionElementAtPoint(point);
if (this.pressedSuggestionElement != null)
{
// Give the pressed suggestion the hover brushes
this.pressedSuggestionElement.Background =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
(this.pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneBackgroundBrush”] as Brush;
}
else if (point.Y > this.StackPanel.Height)
{
// Treat this as a tap on the down arrow
if (this.DownButtonTap != null)
this.DownButtonTap(this, EventArgs.Empty);
}
}
public void OnMouseMove(Point point)
{
double delta = point.X – this.mouseMoveX;
if (delta == 0)
return;
// Adjust the stack panel’s left margin to simulate scrolling.
// Don’t let it scroll past either its left or right edge.
double newLeft = Math.Min(0, Math.Max(this.ActualWidth –
this.StackPanel.ActualWidth, this.StackPanel.Margin.Left + delta));
this.StackPanel.Margin = new Thickness(newLeft, 0, 0, 0);
// If a suggestion is currently being pressed but we’ve now scrolled a
// certain amount, cancel the tapping action
if (pressedSuggestionElement != null && Math.Abs(this.mouseMoveX
– this.mouseDownX) > Constants.MIN_SCROLL_AMOUNT)
{
// Undo the hover brushes
pressedSuggestionElement.Background = null;
(pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
// Stop tracking the element
pressedSuggestionElement = null;
}
this.mouseMoveX = point.X;
}
public void OnMouseUp(bool isInBounds)
{
if (this.pressedSuggestionElement != null)
{
if (isInBounds)
InsertText();
// Undo the hover brushes
pressedSuggestionElement.Background = null;
(pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
// Stop tracking the element
pressedSuggestionElement = null;
}
}
public void ResetScrollPosition()
{
this.StackPanel.Margin = new Thickness(0, 0, 0, 0);
}
public void ClearSuggestions()
{
this.StackPanel.Children.Clear();
ResetScrollPosition();
}
// Each suggestion is added to the stack panel as two elements:
// – A border containing a textblock with a • separator
// – A border containing the suggested text
public void AddSuggestion(Suggestion suggestion)
{
// Add the • element to the stack panel
TextBlock textBlock = new TextBlock { Text = “•”, FontSize = 16,
Margin = new Thickness(this.StackPanel.Children.Count == 0 ? 20 : 3, 6, 4,
0), Foreground = Application.Current.Resources[“PhoneForegroundBrush”]
as Brush, VerticalAlignment = VerticalAlignment.Center };
Border border = new Border();
border.Child = textBlock;
this.StackPanel.Children.Add(border);
// Add the suggested-text element to the stack panel
textBlock = new TextBlock { Text = suggestion.Text, FontSize = 28,
Margin = new Thickness(10, 6, 10, 0),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = Application.Current.Resources[“PhoneForegroundBrush”]
as Brush };
// MinWidth makes single-character suggestions like / easier to tap
// Stuff the insertion offset into the tag for easy retrieval later
border = new Border { MinWidth = 28, Tag = suggestion.InsertionOffset };
border.Child = textBlock;
this.StackPanel.Children.Add(border);
}
void InsertText()
{
string newText = (this.pressedSuggestionElement.Child as TextBlock).Text;
int numCharsToDelete = ((int)this.pressedSuggestionElement.Tag) * -1;
string allText = this.textBox.Text;
// Perform the insertion
allText = allText.Substring(0, this.selectionStart – numCharsToDelete)
+ newText
+ allText.Substring(this.selectionStart + this.selectionLength);
this.textBox.Text = allText;
// Place the caret immediately after the inserted text
this.textBox.SelectionStart = this.selectionStart + newText.Length –
numCharsToDelete;
}
// Find the Border element at the current point
Border FindSuggestionElementAtPoint(Point point)
{
Border border = null;
// Loop through the borders to find the right one (if there is one)
for (int i = 0; i < this.StackPanel.Children.Count; i++)
{
Border b = this.StackPanel.Children[i] as Border;
// Transform the point to be relative to this border
GeneralTransform generalTransform = this.StackPanel.TransformToVisual(b);
Point pt = generalTransform.Transform(point);
pt.X -= this.StackPanel.Margin.Left; // Adjust for scrolling
// See if the point is within the border’s bounds.
// The extra right margin ensures that there are no “holes” in the bar
// where tapping does nothing.
if (pt.X >= 0 && pt.X < b.ActualWidth + Constants.TAP_MARGIN
&& pt.Y <= this.StackPanel.Height)
{
border = b;
// If this is the • element, treat it as part of the next element
// (the actual word), so return that one instead
if ((b.Child as TextBlock).Text == “•”)
border = this.StackPanel.Children[i + 1] as Border;
break;
}
}
return border;
}
}
}

[/code]

Notes:

The Finished Product

Can I write code that interacts with the phone’s copy & paste feature?

No. Copy & paste functionality is automatically supported for any text box, but there is currently no way for a developer to interact with the clipboard, disable the feature, or otherwise influence its behavior.

 

Exit mobile version