I’m back with my Silverlight for Windows Embedded tutorial. Sorry for the long delay between step 3 and step 4, the MVP summit and some work related issue prevented me from working on the tutorial during the last weeks.
In our first, second and third tutorial steps we implemented some very simple applications, just to understand the basic structure of a Silverlight for Windows Embedded application, learn how to handle events and how to operate on images.
In this third step our sample application will be slightly more complicated, to introduce two new topics: list boxes and custom control. We will also learn how to create controls at runtime.
I choose to explain those topics together and provide a sample a bit more complicated than usual just to start to give the feeling of how a “real” Silverlight for Windows Embedded application is organized.
As usual we can start using Expression Blend to define our main page. In this case we will have a listbox and a textblock.
Here’s the XAML code:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ListDemo.Page"
Width="640" Height="480" x:Name="ListPage" xmlns:ListDemo="clr-namespace:ListDemo">
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Margin="19,57,19,66" x:Name="FileList" SelectionChanged="Filelist_SelectionChanged"/>
<TextBlock Height="35" Margin="19,8,19,0" VerticalAlignment="Top" TextWrapping="Wrap" x:Name="CurrentDir" Text="TextBlock" FontSize="20"/>
</Grid>
</UserControl>
In our listbox we will load a list of directories, starting from the filesystem root (there are no drives in Windows CE, the filesystem has a single root named “\”). When the user clicks on an item inside the list, the corresponding directory path will be displayed in the TextBlock object and the subdirectories of the selected branch will be shown inside the list.
As you can see we declared an event handler for the SelectionChanged event of our listbox.
We also used a different font size for the TextBlock, to make it more readable. XAML and Expression Blend allow you to customize your UI pretty heavily, experiment with the tools and discover how you can completely change the aspect of your application without changing a single line of code!
Inside our ListBox we want to insert the directory presenting a nice icon and their name, just like you are used to see them inside Windows 7 file explorer, for example. To get this we will define a user control. This is a custom object that will behave like “regular” Silverlight for Windows Embedded objects inside our application.
First of all we have to define the look of our custom control, named DirectoryItem, using XAML:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="ListDemo.DirectoryItem" Width="500" Height="80">
<StackPanel x:Name="LayoutRoot" Orientation="Horizontal">
<Canvas Width="31.6667" Height="45.9583" Margin="10,10,10,10" RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-31.27"/>
<TranslateTransform/>
</TransformGroup>
</Canvas.RenderTransform>
<Rectangle Width="31.6667" Height="45.8414" Canvas.Left="0" Canvas.Top="0.116943" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.142631,0.75344" EndPoint="1.01886,0.75344">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.142631" CenterY="0.75344" AngleX="19.3128" AngleY="0"/>
<RotateTransform CenterX="0.142631" CenterY="0.75344" Angle="-35.3436"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF7B6802" Offset="0"/>
<GradientStop Color="#FFF3D42C" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="29.8441" Height="43.1517" Canvas.Left="0.569519" Canvas.Top="1.05249" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.142632,0.753441" EndPoint="1.01886,0.753441">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.142632" CenterY="0.753441" AngleX="19.3127" AngleY="0"/>
<RotateTransform CenterX="0.142632" CenterY="0.753441" Angle="-35.3437"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFCDCDCD" Offset="0.0833333"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="29.8441" Height="43.1517" Canvas.Left="0.455627" Canvas.Top="2.28036" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.142631,0.75344" EndPoint="1.01886,0.75344">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.142631" CenterY="0.75344" AngleX="19.3128" AngleY="0"/>
<RotateTransform CenterX="0.142631" CenterY="0.75344" Angle="-35.3436"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFCDCDCD" Offset="0.0833333"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="29.8441" Height="43.1517" Canvas.Left="0.455627" Canvas.Top="1.34485" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.142631,0.75344" EndPoint="1.01886,0.75344">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.142631" CenterY="0.75344" AngleX="19.3128" AngleY="0"/>
<RotateTransform CenterX="0.142631" CenterY="0.75344" Angle="-35.3436"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFCDCDCD" Offset="0.0833333"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="26.4269" Height="45.8414" Canvas.Left="0.227798" Canvas.Top="0" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.142631,0.75344" EndPoint="1.01886,0.75344">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.142631" CenterY="0.75344" AngleX="19.3127" AngleY="0"/>
<RotateTransform CenterX="0.142631" CenterY="0.75344" Angle="-35.3436"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF7B6802" Offset="0"/>
<GradientStop Color="#FFF3D42C" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="1.25301" Height="45.8414" Canvas.Left="1.70862" Canvas.Top="0.116943" Stretch="Fill" Fill="#FFEBFF07"/>
</Canvas>
<TextBlock Height="80" x:Name="Name" Width="448" TextWrapping="Wrap" VerticalAlignment="Center" FontSize="24" Text="Directory"/>
</StackPanel>
</UserControl>
As you can see, this XAML contains many graphic elements. Those elements are used to design the folder icon. The original drawing has been designed in Expression Design and then exported as XAML.
In Silverlight for Windows Embedded you can use vector images. This means that your images will look good even when scaled or rotated.
In our DirectoryItem custom control we have a TextBlock named Name, that will be used to display….(suspense)…. the directory name (I’m too lazy to invent fancy names for controls, and using “boring” intuitive names will make code more readable, I hope!).
Now that we have some XAML code, we may execute XAML2CPP to generate part of the aplication code for us.
We should then add references to our XAML2CPP generated resource file and include in our code and add a reference to the XAML runtime library to our sources file (you can follow the instruction of the first tutorial step to do that),
To generate the code used in this tutorial you need XAML2CPP ver 1.0.1.0, that is downloadable here:
http://geekswithblogs.net/WindowsEmbeddedCookbook/archive/2010/03/08/xaml2cpp-1.0.1.0.aspx
We can now create our usual simple Win32 application inside Platform Builder, using the same step described in the first chapter of this tutorial (http://geekswithblogs.net/WindowsEmbeddedCookbook/archive/2009/10/01/silverlight-for-embedded-tutorial.aspx).
We can declare a class for our main page, deriving it from the template that XAML2CPP generated for us:
class ListPage : public TListPage<ListPage>
{
...
}
We will see the ListPage class code in a short time, but before we will see the code of our DirectoryItem user control. This object will be used to populate our list, one item for each directory.
To declare a user control things are a bit more complicated (but also in this case XAML2CPP will write most of the “boilerplate” code for use.
To interact with a user control you should declare an interface. An interface defines the functions of a user control that can be called inside the application code. Our custom control is currently quite simple and we just need some member functions to store and retrieve a full pathname inside our control. The control will display just the last part of the path inside the control.
An interface is declared as a C++ class that has only abstract virtual members. It should also have an UUID associated with it. UUID means Universal Unique IDentifier and it’s a 128 bit number that will identify our interface without the need of specifying its fully qualified name. UUIDs are used to identify COM interfaces and, as we discovered in chapter one, Silverlight for Windows Embedded is based on COM or, at least, provides a COM-like Application Programming Interface (API).
Here’s the declaration of the DirectoryItem interface:
class __declspec(novtable,uuid("{D38C66E5-2725-4111-B422-D75B32AA8702}")) IDirectoryItem : public IXRCustomUserControl
{
public:
virtual HRESULT SetFullPath(BSTR fullpath) = 0;
virtual HRESULT GetFullPath(BSTR* retval) = 0;
};
The interface is derived from IXRCustomControl, this will allow us to add our object to a XAML tree.
It declares the two functions needed to set and get the full path, but don’t implement them. Implementation will be done inside the control class. The interface only defines the functions of our control class that are accessible from the outside. It’s a sort of “contract” between our control and the applications that will use it. We must support what’s inside the contract and the application code should know nothing else about our own control.
To reference our interface we will use the UUID, to make code more readable we can declare a #define in this way:
#define IID_IDirectoryItem __uuidof(IDirectoryItem)
Silverlight for Windows Embedded objects (like COM objects) use a reference counting mechanism to handle object destruction. Every time you store a pointer to an object you should call its AddRef function and every time you no longer need that pointer you should call Release. The object keeps an internal counter, incremented for each AddRef and decremented on Release. When the counter reaches 0, the object is destroyed.
Managing reference counting in our code can be quite complicated and, since we are lazy (I am, at least!), we will use a great feature of Silverlight for Windows Embedded: smart pointers.A smart pointer can be connected to a Silverlight for Windows Embedded object and manages its reference counting.
To declare a smart pointer we must use the XRPtr template:
typedef XRPtr<IDirectoryItem> IDirectoryItemPtr;
Now that we have defined our interface, it’s time to implement our user control class.
XAML2CPP has implemented a class for us, and we have only to derive our class from it, defining the main class and interface of our new custom control:
class DirectoryItem : public DirectoryItemUserControlRegister<DirectoryItem,IDirectoryItem>
{
...
}
XAML2CPP has generated some code for us to support the user control, we don’t have to mind too much about that code, since it will be generated (or written by hand, if you like) always in the same way, for every user control. But knowing how does this works “under the hood” is still useful to understand the architecture of Silverlight for Windows Embedded.
Our base class declaration is a bit more complex than the one we used for a simple page in the previous chapters:
template <class A,class B>
class DirectoryItemUserControlRegister : public XRCustomUserControlImpl<A,B>,public TDirectoryItem<A,XAML2CPPUserControl>
{
...
}
This class derives from the XAML2CPP generated template class, like the ListPage class, but it uses XAML2CPPUserControl for the implementation of some features. This class shares the same ancestor of XAML2CPPPage (base class for “regular” XAML pages), XAML2CPPBase, implements binding of member variables and event handlers but, instead of loading and creating its own XAML tree, it attaches to an existing one.
The XAML tree (and UI) of our custom control is created and loaded by the XRCustomUserControlImpl class. This class is part of the Silverlight for Windows Embedded framework and implements most of the functions needed to build-up a custom control in Silverlight (the guys that developed Silverlight for Windows Embedded seem to care about lazy programmers!). We have just to initialize it, providing our class (DirectoryItem) and interface (IDirectoryItem).
Our user control class has also a static member:
protected:
static HINSTANCE hInstance;
This is used to store the HINSTANCE of the modules that contain our user control class. I don’t like this implementation, but I can’t find a better one, so if somebody has good ideas about how to handle the HINSTANCE object, I’ll be happy to hear suggestions!
It also implements two static members required by XRCustomUserControlImpl.
The first one is used to load the XAML UI of our custom control:
static HRESULT GetXamlSource(XRXamlSource* pXamlSource)
{
pXamlSource->SetResource(hInstance,TEXT("XAML"),IDR_XAML_DirectoryItem);
return S_OK;
}
It initializes a XRXamlSource object, connecting it to the XAML resource that XAML2CPP has included in our resource script.
The other method is used to register our custom control, allowing Silverlight for Windows Embedded to create it when it load some XAML or when an application creates a new control at runtime (more about this later):
static HRESULT Register()
{
return XRCustomUserControlImpl<A,B>::Register(__uuidof(B), L"DirectoryItem", L"clr-namespace:DirectoryItemNamespace");
}
To register our control we should provide its interface UUID, the name of the corresponding element in the XAML tree and its current namespace (namespaces compatible with Silverlight must use the “clr-namespace” prefix.
We may also register additional properties for our objects, allowing them to be loaded and saved inside XAML. In this case we have no permanent properties and the Register method will just register our control.
An additional static method is implemented to allow easy registration of our custom control inside our application WinMain function:
static HRESULT RegisterUserControl(HINSTANCE hInstance)
{
DirectoryItemUserControlRegister::hInstance=hInstance;
return DirectoryItemUserControlRegister<A,B>::Register();
}
Now our control is registered and we will be able to create it using the Silverlight for Windows Embedded runtime functions.
But we need to bind our members and event handlers to have them available like we are used to do for other XAML2CPP generated objects. To bind events and members we need to implement the On_Loaded function:
virtual HRESULT OnLoaded(__in IXRDependencyObject* pRoot)
{
HRESULT retcode;
IXRApplicationPtr app;
if (FAILED(retcode=GetXRApplicationInstance(&app)))
return retcode;
return ((A*)this)->Init(pRoot,hInstance,app);
}
This function will call the XAML2CPPUserControl::Init member that will connect the “root” member with the XAML sub tree that has been created for our control and then calls BindObjects and BindEvents to bind members and events to our code.
Now we can go back to our application code (the code that you’ll have to actually write) to see the contents of our DirectoryItem class:
class DirectoryItem : public DirectoryItemUserControlRegister<DirectoryItem,IDirectoryItem>
{
protected:
WCHAR fullpath[_MAX_PATH+1];
public:
DirectoryItem()
{
*fullpath=0;
}
virtual HRESULT SetFullPath(BSTR fullpath)
{
wcscpy_s(this->fullpath,fullpath);
WCHAR* p=fullpath;
for(WCHAR*q=wcsstr(p,L"\\");q;p=q+1,q=wcsstr(p,L"\\"))
;
Name->SetText(p);
return S_OK;
}
virtual HRESULT GetFullPath(BSTR* retval)
{
*retval=SysAllocString(fullpath);
return S_OK;
}
};
It’s pretty easy and contains a fullpath member (used to store that path of the directory connected with the user control) and the implementation of the two interface members that can be used to set and retrieve the path.
The SetFullPath member parses the full path and displays just the last branch directory name inside the “Name” TextBlock object.
As you can see, implementing a user control in Silverlight for Windows Embedded is not too complex and using XAML also for the UI of the control allows us to re-use the same mechanisms that we learnt and used in the previous steps of our tutorial.
Now let’s see how the main page is managed by the ListPage class.
class ListPage : public TListPage<ListPage>
{
protected:
// current path
TCHAR curpath[_MAX_PATH+1];
It has a member named “curpath” that is used to store the current directory.
It’s initialized inside the constructor:
ListPage()
{
*curpath=0;
}
And it’s value is displayed inside the “CurrentDir” TextBlock inside the initialization function:
virtual HRESULT Init(HINSTANCE hInstance,IXRApplication* app)
{
HRESULT retcode;
if (FAILED(retcode=TListPage<ListPage>::Init(hInstance,app)))
return retcode;
CurrentDir->SetText(L"\\");
return S_OK;
}
The FillFileList function is used to enumerate subdirectories of the current dir and add entries for each one inside the list box that fills most of the client area of our main page:
HRESULT FillFileList()
{
HRESULT retcode;
IXRItemCollectionPtr items;
IXRApplicationPtr app;
if (FAILED(retcode=GetXRApplicationInstance(&app)))
return retcode;
// retrieves the items contained in the listbox
if (FAILED(retcode=FileList->GetItems(&items)))
return retcode;
// clears the list
if (FAILED(retcode=items->Clear()))
return retcode;
// enumerates files and directory in the current path
WCHAR filemask[_MAX_PATH+1];
wcscpy_s(filemask,curpath);
wcscat_s(filemask,L"\\*.*");
WIN32_FIND_DATA finddata;
HANDLE findhandle;
findhandle=FindFirstFile(filemask,&finddata);
// the directory is empty?
if (findhandle==INVALID_HANDLE_VALUE)
return S_OK;
do
{
if (finddata.dwFileAttributes&=FILE_ATTRIBUTE_DIRECTORY)
{
IXRListBoxItemPtr listboxitem;
// add a new item to the listbox
if (FAILED(retcode=app->CreateObject(IID_IXRListBoxItem,&listboxitem)))
{
FindClose(findhandle);
return retcode;
}
if (FAILED(retcode=items->Add(listboxitem,NULL)))
{
FindClose(findhandle);
return retcode;
}
IDirectoryItemPtr directoryitem;
if (FAILED(retcode=app->CreateObject(IID_IDirectoryItem,&directoryitem)))
{
FindClose(findhandle);
return retcode;
}
WCHAR fullpath[_MAX_PATH+1];
wcscpy_s(fullpath,curpath);
wcscat_s(fullpath,L"\\");
wcscat_s(fullpath,finddata.cFileName);
if (FAILED(retcode=directoryitem->SetFullPath(fullpath)))
{
FindClose(findhandle);
return retcode;
}
XAML2CPPXRValue value((IXRDependencyObject*)directoryitem);
if (FAILED(retcode=listboxitem->SetContent(&value)))
{
FindClose(findhandle);
return retcode;
}
}
}
while (FindNextFile(findhandle,&finddata));
FindClose(findhandle);
return S_OK;
}
This functions retrieve a pointer to the collection of the items contained in the directory listbox. The IXRItemCollection interface is used by listboxes and comboboxes and allow you to clear the list (using Clear(), as our function does at the beginning) and change its contents by adding and removing elements.
This function uses the FindFirstFile/FindNextFile functions to enumerate all the objects inside our current directory and for each subdirectory creates a IXRListBoxItem object. You can insert any kind of control inside a list box, you don’t need a IXRListBoxItem, but using it will allow you to handle the selected state of an item, highlighting it inside the list.
The function creates a list box item using the CreateObject function of XRApplication. The same function is then used to create an instance of our custom control. The function returns a pointer to the control IDirectoryItem interface and we can use it to store the directory full path inside the object and add it as content of the IXRListBox item object, adding it to the listbox contents.
The listbox generates an event (SelectionChanged) each time the user clicks on one of the items contained in the listbox.
We implement an event handler for that event and use it to change our current directory and repopulate the listbox. The current directory full path will be displayed in the TextBlock:
HRESULT Filelist_SelectionChanged(IXRDependencyObject* source,XRSelectionChangedEventArgs* args)
{
HRESULT retcode;
IXRListBoxItemPtr listboxitem;
if (!args->pAddedItem)
return S_OK;
if (FAILED(retcode=args->pAddedItem->QueryInterface(IID_IXRListBoxItem,(void**)&listboxitem)))
return retcode;
XRValue content;
if (FAILED(retcode=listboxitem->GetContent(&content)))
return retcode;
if (content.vType!=VTYPE_OBJECT)
return E_FAIL;
IDirectoryItemPtr directoryitem;
if (FAILED(retcode=content.pObjectVal->QueryInterface(IID_IDirectoryItem,(void**)&directoryitem)))
return retcode;
content.pObjectVal->Release();
content.pObjectVal=NULL;
BSTR fullpath=NULL;
if (FAILED(retcode=directoryitem->GetFullPath(&fullpath)))
return retcode;
CurrentDir->SetText(fullpath);
wcscpy_s(curpath,fullpath);
FillFileList();
SysFreeString(fullpath);
return S_OK;
}
};
The function uses the pAddedItem member of the XRSelectionChangedEventArgs object to retrieve the currently selected item, converts it to a IXRListBoxItem interface using QueryInterface, and then retrives its contents (IDirectoryItem object). Using the GetFullPath method we can get the full path of our selected directory and assing it to the curdir member. A call to FillFileList will update the listbox contents, displaying the list of subdirectories of the selected folder.
To build our sample we just need to add code to our WinMain function:
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
if (!XamlRuntimeInitialize())
return -1;
HRESULT retcode;
IXRApplicationPtr app;
if (FAILED(retcode=GetXRApplicationInstance(&app)))
return -1;
if (FAILED(retcode=DirectoryItem::RegisterUserControl(hInstance)))
return retcode;
ListPage page;
if (FAILED(page.Init(hInstance,app)))
return -1;
page.FillFileList();
UINT exitcode;
if (FAILED(page.GetVisualHost()->StartDialog(&exitcode)))
return -1;
return 0;
}
This code is very similar to the one of the WinMains of our previous samples. The main differences are that we register our custom control (you should do that as soon as you have initialized the XAML runtime) and call FillFileList after the initialization of our ListPage object to load the contents of the root folder of our device inside the listbox.
As usual you can download the full sample source code from here:
http://cid-9b7b0aefe3514dc5.skydrive.live.com/self.aspx/.Public/ListBoxTest.zip