Wednesday 6 April 2011

Creating a clickable TextBlock (or any control) in WP7 using a Custom Control

I came across a need on my current windows phone 7 project to have different parts of my item in a listbox clickable with different events. Plus I’m using MVVM Light. Sounds easy, right? Well I thought so until I realised that the TextBlock control doesn’t have a click event.

First thought was putting the TextBlock into a button as content. This works, although you get this inverted effect on clicking which really didn’t look good on the controls I had. I could change this effect using states, but the button still clicked when you happened to be dragging and the starting point was the button.

Next I looked at the GestureService/Listener which is included in the Silverlight Toolkit. This is very useful but doesn’t allow binding to MVVM Light using eventToCommand behaviours.
Then I looked at a TextBlock having MouseLeftButtonDown & Up events which I could capture. But that still fires even if I’m just dragging the list around but happened to start on the control.
So I figured I could get around this by tracking the point of click. My app is using MVVM however and I didn’t want to start putting either
  1. Mouse event handling code in my ViewModel
  2. Lots of events in my View
So instead I thought I’d create a custom control that has a data bindable click event.
Whilst creating this reusable control I thought I’d add the ability to choose it’s click detection type.
  • Point based would look to see if your finger/mouse is in the same spot when you press down and then up (good for scrolling)
  • Border based would look to see if you’re in the bounds of the zone (like a traditional button).
Here’s the steps I took:
Create the new a Windows Phone Class Library project. This will be where the control lives.
image
Rename the Class1.cs using the Solution Explorer to your new control name. In this instance I’ve called it ClickZone.
Just above the class declaration (or in a separate file if preferred) add a public enumeration to identify the possible click types.
This control will inherit from the ContentControl. This will easily allow it to contain content (other controls), and will act as a click zone for them all.
Add a private Point variable to track the point when the finger/mouse is pressed down. Also add a boolean flag to track when the finger/mouse is in the control.
Your code should look similar to this now:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;

namespace CustControlTest
{
public enum ClickDetectType { PointBased, BorderBased };

public class ClickZone : ContentControl
{
//Reference for point based click
private Point startPoint = new Point();

//Flag for border based click
private bool isInBorder;
}
}


Now to add a click Event which can be captured, and a public property for the detection type.


public event EventHandler Click;
protected virtual void OnClick()
{
EventHandler handler = Click;
if (handler != null)
handler(this, new EventArgs());
}

private ClickDetectType _detectionType = ClickDetectType.PointBased;
public ClickDetectType DetectionType
{
get { return _detectionType; }
set { _detectionType = value; }
}



Next I need to capture the key mouse/finger events and track when it’s entering or leaving the control and when pressing/releasing. It’s on the release that I need to decide whether to fire the Click event or not.


protected override void OnMouseLeave(MouseEventArgs e)
{
//track border exit
isInBorder = false;
base.OnMouseLeave(e);
}

protected override void OnMouseEnter(MouseEventArgs e)
{
//track border entry
isInBorder = true;
base.OnMouseEnter(e);
}

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
//track point entry
startPoint = e.GetPosition(null);
base.OnMouseLeftButtonDown(e);
}

protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
switch (_detectionType)
{
case ClickDetectType.BorderBased:
if (isInBorder)
OnClick();
break;
case ClickDetectType.PointBased:
if (startPoint.X == e.GetPosition(null).X && startPoint.Y == e.GetPosition(null).Y)
OnClick();
break;
}

isInBorder = false;
startPoint = new Point();

base.OnMouseLeftButtonUp(e);
}



Now that’s the class done, but it still needs a template. This is where you need a ResourceDictionary xaml file. Create a folder called Themes, and in it add a new xaml file called generic.xaml. Make sure you switch the Build Action to Resource, as it needs to be compiled into the DLL.


image


For my control layout I went for a simple structure of a Border control, with the content inside inside it. For more details on creating a ResourceDictionary, see the links at the bottom.


<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:controls="clr-namespace:CustControlTest">
<Style TargetType="controls:ClickZone">
<Setter Property="Background" 
Value="Transparent"/>
<Setter Property="BorderBrush" 
Value="{StaticResource PhoneTextBoxBrush}"/>
<Setter Property="BorderThickness" 
Value="{StaticResource PhoneBorderThickness}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:ClickZone">
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter
x:Name="ContentContainer"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>



Using the TemplateBindings meant that the properties can be changed when reusing the control later.



Hopefully that should all compile fine now. So to test, create a new Project and reference this project. I’m going to use an MVVM Light one to show binding. You should now see the ClickZone control in the Toolbox


image


You can now drop this control on your page, and put content inside it. For this I’ll use Blend to show Blendability.


image


As you can see, I am able to add an EventToCommand behaviour to the ClickZone.


You can see on the Properties of the EventToCommand that I can now bind to the Click event.


image






Finally adding a command to the ViewModel which updates a property I can see the result!


image


Links I found useful:


http://www.windowsphonegeek.com/articles/Creating-a-WP7-Custom-Control-in-7-Steps


http://www.windowsphonegeek.com/articles/WP7-WatermarkedTextBox-custom-control


http://msdn.microsoft.com/en-us/library/system.windows.dependencyproperty%28v=vs.95%29.aspx


I have included all the code and sample MVVM Light application in the following file: