Single Sign On with Linux VDA 1.1

Customers want things to be easier. A common request is to support Single Sign On (SSO).  Instead of having to enter credentials more than once, SSO remembers the username and password. The SSO design is secure yet provides a better user experience. SSO is a common feature for web browsers. Once logged in, the credentials are used for the site without requiring entering them again.

In Citrix XenDesktop/XenApp, the idea is very similar.  Once logged into Receiver, StoreFront, or Web Interface, the user does not need to enter passwords again for desktop sessions and applications.  Users only need to click on the icon of the desired desktop or application and the connection will be made without requiring credentials.  Even though many different hosts are involved, the hosts work together to allow the user entry.

However, this is not considered the ultimate SSO solution.  For users using Windows Receiver, there is a feature that uses the native Windows credentials from the user’s session.  If the user is logged into Active Directory on a Windows domain workstation, they will not be required to login to Citrix if SSO is properly configured.

Windows Receiver

The official title for the Citrix feature is “Pass Thru Authentication” and based on the latest feature chart, is only supported on Windows Receiver.  Setting it up is more involved than a typical installation.  Please review the knowledge article. There is a command line switch called “/includeSSON” when installing CitrixReceiver.exe.

sson

There are several other steps so please follow the knowledge article.

Once everything is working, users only need to sign into Windows in order to use Citrix XenApp and XenDesktop.

In Linux VDA version 1.0, we did not support this feature.  To understand why requires some basic understanding of how this technology works.  We will get to that in a moment, but it is important to announce that we have added this feature in Linux VDA version 1.1.  This version of the Linux VDA can use the user’s domain credentials which are made available by the machine that the user is logged into and is running Windows Receiver.

Inside Citrix, we call this support “WD Credentials”.  WD stands for Winstation Driver, a core module in the Citrix software stack. The concept is that the credentials are provided during the connection from Citrix Receiver. These credentials are provided very early in the HDX/ICA connection between Receiver and the host (Linux VDA). In the original model, the credential data was gathered from tickets in the ICA file. Specifically, the ticket was used to request the credentials from the Citrix DDC (broker).  This was a problem since it is not possible to get Windows Receiver SSO credentials this way.

HTML5 Receiver

Over time, “WD Credentials” has been used by other Receivers as well.  In fact, some Receivers only support providing credential information this way.  For example, the Citrix HTML5 Receiver passes the user’s credentials to the VDA when requested.  If the VDA does not request it (and uses the ICA file only) HTML5 Receiver will never be able to automatically sign in. In order to support Linux VDA 1.0, HTML5 Receiver 16.0 added support for the old credential transfer.  However, this still meant that the older HTML5 Receivers could not connect and login automatically.  With the change in Linux VDA 1.1, this is no longer a problem.

Smart card

Even though the Linux VDA now supports “WD Credentials” it does not yet support smart cards.  There is an additional work item to support the smart card virtual channel.

Linux VDA

One of the hidden benefits of SSO is Active Directory integration. The credentials work for Windows resources accessed from Linux. For example, SSO allows for automatic access to user home directories. Services based on Active Directory Kerberos respect that the user has been authorized and will not request credentials from the user.  This reduces the pain of connecting network shares during login.

Summary

Linux VDA 1.1 now supports Windows Receiver SSO and all Receivers that use “WD Credentials” to exchange credentials.  This includes HTML5 Receivers prior to version 16.0. Smart card support is not there yet. With the Linux VDA 1.1 release, true SSO with Windows is now possible and the Receivers are able to use the latest Citrix credential exchange technology.

To read more from the Linux Virtual Desktop Team, please refer to the Linux Virtual Desktop Team blog here.

Linux VDA 1.1 Internationalization (UTF-8)

In the history of computing, one aspect that had eluded capture is a consistent encoding scheme.  There have been many attempts to standardize a character encoding scheme, but each has had strong downsides until about 20 years ago.  The invention of UTF-8 in 1992 by Ken Thompson based on Unicode was the solution to the original problem of universal encoding. It provided the ability to represent all possible characters in the smallest space possible.  It also preserved compatibility with ASCII. Other solutions did not have full coverage or used too much storage.

Encoding Year Adopted Bytes/Char Description
ASCII 1964 1 Early standard. Made popular by PC
UCS-2 1993 2 First Unicode encoding
UTF-16 1996 2,4 UCS-2 model with more chars
UTF-32 1996 4 Simple, but uses much memory
UTF-8 1998 1-4 Most efficient and compatible

Based on the timing of how things happened, Windows adopted UCS-2 first with Windows NT in 1993. This was seen as an improvement over the traditional single byte character sets of DOS and Windows 3.x. Unfortunately, this added complexity to Windows development and support due to the dual model of Unicode and non-Unicode support. It led to the classic doubling of Windows APIs. One version of the API appended with ‘W’ (Wide/Unicode) and the other ‘A’ (ASCII). A brief explanation can be found here.

Use of A and W in Windows

Citrix XenApp and XenDesktop are built on Windows models. Even Citrix Linux VDA has its roots in the Windows encoding schemes. Overall this makes sense because it is the history of what happened and it made sense to share code between platforms.

UTF8

This philosophy has changed for Linux VDA version 1.1. Instead of trying to preserve the Microsoft encoding schemes, everything is now converted internally to UTF-8. Initially this might not seem to have much relevance to customers. However, this provides some immediate benefits and also some longer term improvements.

Because UTF-8 is now the core encoding in Linux VDA, it no longer has to convert strings internally. This improves performance slightly and also reduces the risk of losing something in translation. It also reduces the footprint of how much space the strings need.

Another benefit is that it allows for a native encoding on Linux. Messages coming from administrators are now allowed to be displayed using full Unicode support. Even though the message arrives in UTF-16 from Studio, the message is converted to UTF-8 and displayed using GTK+.

Yet another reason to use UTF-8 is that it is now possible to support full Unicode text transfers with the clipboard. Again, even though the clipboard is receiving UTF-16 text, it is automatically translated to UTF-8 for the sake of the Linux applications. This is very important for Asian languages that typically have large character sets.

UTF8Clipboard

A side benefit is improved username handling. It is now possible to support usernames that include non-ASCII characters. This was tried as part of the Linux VDA 1.1 tests. The username in that case had characters above the BMP range (>64K) which is considered fairly rare but valid.

Beyond these changes, the logging and tracing components now use UTF-8. This allows for full character set usage for log messages and trace output. There is still more work needed to localize the log messages to non-English languages but at least it is enabled and will display UTF-8 content.

Internally it simplified the code in many places. This will allow for more consistent handling of the strings and less trouble with conversion.

As to the future, it provides a base for Linux VDA to better support any language. It also allows for the possibility of having a server that supports different languages at once with different users. The overall biggest potential is full integration with Linux related to text.

To read more from the Linux Virtual Desktop Team, please refer to the Linux Virtual Desktop Team blog here.

Linux and Citrix

To start with, this is not an official statement from Citrix.  Rather, this is a collection of observations over the length of time at Citrix.

DSCN1637

Citrix Linux sunrise

First thing to note is that in the history of Citrix, the company had respect for Unix/Linux, and the first product in 1991 was effectively an OS/2 version of supporting terminals.  This model matched the classic Unix terminal framework.  The vision was that Citrix would provide a solution with Microsoft-based applications to terminals.  A lofty goal for 1989 when it was first formed.

However, there was a dark cloud present.  Any consideration between 1989 and the last few years to use Linux in a server product was seen as conflict of interest related to the relationship with Microsoft.

In 1998, Citrix acquired Insignia.  This was key to releasing clients that supported Mac, Linux, and Unix.  The dividends for this continue to this day.  This Linux Receiver is seen as one of the more important receivers and is still updated with relevant features.

Also in 1998, I worked as a system engineer for a reseller in Brisbane.  The experience revealed plenty of direct customer exposure and the joys of supporting software that you cannot personally fix.  Anyways, some of the customers were using Linux.  In general, there were two camps.  The government camp wanted to use Linux because it was cheap and they had the talent to support it (or so they thought).  The second camp saw it as a powerful tool for engineers and designers.  There was a bit of mistrust from the government group with any outside opinions.  In my case, I was only supporting a trial of WinFrame for a much larger environment.  Amazingly, they were using Samba with Linux as the backbone of their Windows desktops.  Perhaps you can see the fun in that in the late 90’s.

The second group of people using Linux for workstations for engineers was much more interesting.  Citrix had nothing for that at the time (as a server).  As explained by the customer, they wanted a Linux server that could remote applications to users.  It would not be until 2015, that this would be possible with a Citrix product.

For 1999, the MetaFrame for Unix product started development.  The focus was to create a Unix-based server for Citrix.  The release gained some momentum but avoided Linux.  Even though not declared, the reason was probably to avoid upsetting Microsoft.  In 2014, the core MFU/XAU codebase would form the early basis for the current Citrix Linux VDA.

In 2004, Citrix pursued SSL VPN companies to provide its first VPN solution.  As part of this process, Net6 was acquired.  The secret was that the Net6 appliance was actually running open source Linux.  There were initially concerns about Microsoft’s reaction.  However, the concerns were unfounded due the appliance not being the same model as Microsoft’s client/server environment.

An important 2005 acquisition was Netscaler.  It too was based on a Linux appliance.

In 2007, Citrix acquired XenSource.  The core of Xen virtualizations is Linux.  This introduced the concept of Linux being an acceptable solution for virtualization.

In 2014, Ericsson approached Citrix to produce what was later called Linux VDA.  The Citrix Labs team accepted the task and released the first version in June 2015.  The model follows the design of Windows VDA, but customized to Linux hosts.  Citrix receivers connect to Linux VDA the same way that they connect to Windows VDA.  Given that the receiver is a recent one, it is designed to connect to either server without special treatment.

And to wrap things up, today Microsoft announced that they have developed a Linux network appliance for Azure.  The age of upsetting Microsoft about using Linux is truly over.

Lights, Camera, Action! MobilePicture using the SDK

MobilePictureSample

Taking pictures with mobile devices is common .  The quality of the pictures might not be as good and it might not be always as easy to zoom,  control the flash,  or properly focus, but it does its job well enough in most situations.  The biggest advantage is that you always have your phone so you can capture images that normally would be missed.

When we started working on the Mobile SDK for Windows Apps in 2011, one of the most interesting use cases was being able to take pictures on the mobile device and then using these pictures with the Windows app.  From a customer point of view, this is one of the most compelling reasons to implement an application using the SDK.

What makes this different from usual?  Why would you do it this way?

The answer requires a bit of background information.  The biggest difference is where the picture is first processed.  Instead of being controlled by a local application on the device, the picture is provided to the remote application first.  Since the application is running on XenDesktop/XenApp, it uses the SDK to trigger the picture being taken and collecting the picture when it is done.  This makes it possible to inject a picture directly into a Windows app from a mobile device (phone or tablet).

A number of use cases came to mind early on including maintenance and insurance.  It is possible to not only instantly take pictures at remote locations but to also have these pictures be included in the corporate databases in a very quick way.  The most famous use of this model is from Warren Simondson from Ctrl-Alt-Del.  In this case, the camera is used make sure that miners are where they say they are.

Several months ago, I started working on a project to share on the blog.  This work was meant to extend what was done for MobileHelloWorld. Even though I had made great progress, I never quite got it to the state where I wanted to release it to the world.  The intent was to make it all about pictures and taking new ones.  It uses the SDK more than MobileHelloWorld and has a number of useful features.

  • Enumerates all pictures under My Pictures and puts them in a list for display
  • Navigates backwards and forwards using arrow buttons
  • Rotates picture based on user using rotate buttons
  • Slideshow mode by holding forward or backward buttons for a few seconds
  • Touch the picture will hide/show the controls using a toggle
  • Pictures with Apple orientation are presented correctly
  • Changes to rotation can be saved
  • Pictures are shown as true size if they fit.  If too large, they are zoomed down to the largest fitting size.
  • Label for photo at top right displays name, number, and dimensions
  • New pictures can be taken with the camera button
  • Device rotation will trigger a replacement of controls
  • Control placement is based on screen relative position (top, middle, bottom, left, middle, bottom)
  • Control size is adjusted based on the device type (phone, tablet, PC)
  • The app remembers the last displayed photo and uses it for the next time
  • Icons come from the Visual Studio resource kit and are safe to distribute
  • Delete pictures that are not wanted

My favorite features are the slideshow and being able to capture new pictures.  I also appreciate the ability to correctly display pictures from iOS devices.  Without the code to detect true orientation, it can be quite frustrating.

Instead of dragging you through all the code like I did for HelloWorld, I will instead offer the source code first.

download

There is a lot of potential for using this program as a template for something much more interesting.  This code works fine with the emulator so it is possible to simulate on a Windows 7/8 dev machine with the SDK.  In fact, that is the best place to start since it can be difficult getting a dev server going.

To recap, the program can display pictures from your My Pictures folder and add new ones using the mobile device to capture them.

Mobile WinForms Hello World

In the last post, it discussed how to create a simple Hello World example using WinForms.  The example displays the words “Hello World” in the client area of the window and it calculates how big to make the text based on the area available.  The code is not using any Citrix API extensions to make it run properly on a mobile device.

HelloWorld

There a few things that need to change to get it to be mobile friendly.  Here is the list of things that need to be added:

  1. Reference Mobile SDK for Windows Apps Citrix.CMP.dll assembly
  2. Border style changed to none
  3. Add “Using Citrix.Cmp;” at top
  4. Initialize the SDK object and hook certain events
  5. Add conditional code for mobile and non-mobile paths
  6. Whenever the display changes size, be sure to update the text

There are a few more details to understand but these are the main ideas of what needs to change.

The modified code is included here inline to compare against the previous post.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Citrix.Cmp;
using System.Diagnostics;

namespace MobileHelloWorld
{
    /// <summary>
    /// The MobileHelloWorld form demonstrates how to display "Hello World" with the appropriate font based on the size of the client area.
    /// This work was derived from HelloWorld and extended to use the Mobile SDK for Windows Apps
    /// </summary>
    public partial class MobileHelloWorld : Form
    {
        private CmpApi cmpApi = null;
        private bool mobileDevice = false;

        /// <summary>
        /// Main constructor for MobileHelloWorld Form
        /// </summary>
        public MobileHelloWorld()
        {
            // initialize the designer standard components
            InitializeComponent();

            // initialize the Mobile SDK so we can use it
            InitializeMobileSDK();

            // change how the form looks based on what kind of device it is
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;
            }
            else
            {
                // make sure that this window is maximized on non-mobile devices
                WindowState = FormWindowState.Maximized;
            }
        }

        /// <summary>
        /// Initialize the Mobile SDK API object
        /// </summary>
        private void InitializeMobileSDK()
        {
            try
            {
                // create an instance of our API (referenced from Citrix.Cmp.dll assembly)
                cmpApi = new CmpApi();

                // hook the revelant events to support our app

                // viewport is the area we can use for our application
                cmpApi.Display.ViewportInfoChanged += new EventHandler<ViewportInfoChangedArgs>(Display_ViewportInfoChanged);

                // request notification for when we are connected and disconnected to properly handle the mobile device
                cmpApi.SessionManager.SessionConnected += new EventHandler<SessionConnectedArgs>(SessionManager_SessionConnected);
                cmpApi.SessionManager.SessionDisconnected += new EventHandler(SessionManager_SessionDisconnected);

                // the mobile device is only connected whe IsCmpAvailable is true
                if (cmpApi.SessionManager.IsCmpAvailable)
                {
                    mobileDevice = true;
                }
            }
            catch
            {
                // we did not get a working instance of CmpApi.
                // fail gracefully to allow it to work fine on non-mobile devices
            }
        }

        /// <summary>
        /// Session disconnected event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionDisconnected(object sender, EventArgs e)
        {
            // the mobile device is now no longer connected
            mobileDevice = false;
        }

        /// <summary>
        /// Notified when session is connected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionConnected(object sender, SessionConnectedArgs e)
        {
            // if the mobile device has returned, then allow mobile device calls again
            if (cmpApi.SessionManager.IsCmpAvailable)
            {
                mobileDevice = true;
            }

            // resize the form to match the current connected device
            ResizeForm();
        }

        /// <summary>
        /// Viewport changed event - we are interested in the ClientViewport and use it to resize the main form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Display_ViewportInfoChanged(object sender, ViewportInfoChangedArgs e)
        {
            ViewportInfo viewport = e.NewState;

            if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0.0) && (viewport.ClientViewport.Value.Height != 0.0))
            {
                Size = new Size(Convert.ToInt32(viewport.ClientViewport.Value.Width), Convert.ToInt32(viewport.ClientViewport.Value.Height));

                Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
            }
            else
            {
                if (viewport.ClientViewport.HasValue)
                {
                    Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
                }
                else
                {
                    Trace.WriteLine("ViewportInfoChanged ClientViewport has no value");
                }
            }

            // if the server viewport has changed, make sure our app is at the origin
            if ((viewport.ServerViewport.HasValue) && ((viewport.ServerViewport.Value.X != 0) || (viewport.ServerViewport.Value.Y != 0)))
            {
                Location = new Point(Convert.ToInt32(viewport.ServerViewport.Value.X), Convert.ToInt32(viewport.ServerViewport.Value.Y));
            }
            else
            {
                Location = new Point();
            }
        }

        /// <summary>
        /// Get the current display size for the mobile device.
        /// There are three techniques to attempt in a certain order.
        /// 1. Viewport information
        /// 2. Display state
        /// 3. Actual screen width and height
        /// The viewport is best since it takes into account keyboard and other mobile device areas which are reserved.
        /// The display state is from receiver but does not take into account keyboard or other reserved areas.
        /// The last is the real screen size which comes from Windows.
        /// </summary>
        /// <returns>Size of the display area</returns>
        private Size GetDisplaySize()
        {
            // start with the current size as the default
            Size size = Size;

            if (IsMobileDevice())
            {
                try
                {
                    ViewportInfo viewport = cmpApi.Display.GetViewport();

                    // ViewportInfo.ClientViewport is a nullable value and that means that we have to use HasValue to check it
                    // Also, the iOS Receiver sometimes reports 0,0 we need to check for that too
                    if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0) && (viewport.ClientViewport.Value.Height != 0))
                    {
                        size.Width = Convert.ToInt32(viewport.ClientViewport.Value.Width);
                        size.Height = Convert.ToInt32(viewport.ClientViewport.Value.Height);
                    }
                    else
                    {
                        // The display state resolution is a width, height measurement in pixels
                        DisplayState displayState = cmpApi.Display.GetDisplayState();

                        if (displayState.Resolution.HasValue)
                        {
                            // the display state is the next best place to go
                            size.Width = Convert.ToInt32(displayState.Resolution.Value.Height);
                            size.Height = Convert.ToInt32(displayState.Resolution.Value.Width);
                        }
                        else
                        {
                            // last resort is the screen width and height
                            size.Width = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenWidth);
                            size.Height = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenHeight);
                        }
                    }
                }
                catch
                {
                    // fail gracefully
                }
            }

            return (size);
        }

        /// <summary>
        /// Determines if mobile device is on the other side
        /// </summary>
        /// <returns>true - mobile device, false - non-mobile device</returns>
        private bool IsMobileDevice()
        {
            return (mobileDevice);
        }

        /// <summary>
        /// Called when the form is first loaded.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender">Ignored</param>
        /// <param name="e">Ignored</param>
        private void HelloWorld_Load(object sender, EventArgs e)
        {
            // resize and relocate based on mobile device informtation
            ResizeForm();
        }

        /// <summary>
        /// Resize the Form if it is running on mobile device
        /// </summary>
        private void ResizeForm()
        {
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;

                // Location is always (0,0)
                Location = new Point();

                // Size is calculated from the Mobile SDK information
                Size = GetDisplaySize();
            }
            else
            {
                WindowState = FormWindowState.Maximized;
            }
        }

        /// <summary>
        /// Called when the form is resized.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HelloWorld_Resize(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Resize the Hello World to fill the client area
        /// </summary>
        void ResizeHelloWorld()
        {
            // change the font to match the size of the client area
            HelloWorldLabel.Font = GetBestFontFit(ClientSize, HelloWorldLabel.Font, HelloWorldLabel.Text, ClientSize.Height, 8f); ;

            // center the label
            CenterHelloWorld();
        }

        /// <summary>
        /// Center the HelloWorldLabel in the client area of MobileHelloWorld form
        /// </summary>
        void CenterHelloWorld()
        {
            HelloWorldLabel.Location = new Point((ClientSize.Width / 2) - (HelloWorldLabel.Width / 2), (ClientSize.Height / 2) - (HelloWorldLabel.Height / 2));
        }

        /// <summary>
        /// Determine the best font match for the size/text.  The result should be between the maximum and minimum height.
        /// The size is the area to fill.  The new font is based on the old font which is passed in.
        /// </summary>
        /// <returns>Font</returns>
        public static Font GetBestFontFit(Size size, Font currFont, String text, float fontMax, float fontMin)
        {
            float fontPixels = fontMax;
            float minfontPixels = fontMin;

            // find the corresponding font size for the textbox size
            Font font = currFont;

            // make sure that the text is not null or empty before attempting to fit it in the area.
            // if it is null or empty, the current font is used since it does not matter.
            if (!String.IsNullOrEmpty(text))
            {
                Size textsize;

                font = new Font(currFont.FontFamily, fontPixels, currFont.Style, GraphicsUnit.Pixel);

                textsize = TextRenderer.MeasureText(text, font);

                // Check to see if the font fits in the area and if not, find a smaller one
                while ((((textsize.Height) > size.Height) || ((textsize.Width) > size.Width)) && (fontPixels > minfontPixels))
                {
                    // try to speed up the process of finding the right font size by detecting how much it is too big by
                    if (textsize.Height > size.Height)
                    {
                        // reduce the fontPixels based on how far off the font size is
                        fontPixels -= (textsize.Height - size.Height);
                    }
                    else if (textsize.Width > size.Width)
                    {
                        int charPixelWidth = textsize.Width / text.Length;
                        int targetPixelWidth = size.Width / text.Length;

                        // if the characters are too wide, reduce the fontPixels by how different the result is from the desired width
                        if (charPixelWidth > targetPixelWidth)
                        {
                            fontPixels -= (charPixelWidth - targetPixelWidth);
                        }
                        else
                        {
                            fontPixels -= 1.0F;
                        }
                    }
                    else
                    {
                        fontPixels -= 1.0F;
                    }

                    // drop the old one since it did not work
                    font.Dispose();

                    // get a new font based on the smaller font pixels size
                    font = new Font(font.FontFamily, fontPixels, font.Style, GraphicsUnit.Pixel);

                    // recalculate the size based on the new font
                    textsize = TextRenderer.MeasureText(text, font);
                }
            }

            // when all done, return the font to use
            return (font);
        }

    }
}

MobileHelloWorld

The picture above is the result of running the code against the emulator with the iPhone 5 selected. Note that there is no border and that the size matches an iPhone 5 screen.

Now is a good time to break up what the differences are. There are many new lines but these changes are not conceptually very big.

using Citrix.Cmp;

Including a ‘Using’ line just makes it easier to use routines from Citrix.Cmp namespace. It is not required but a “nice to have”.

    /// <summary>
    /// The MobileHelloWorld form demonstrates how to display "Hello World" with the appropriate font based on the size of the client area.
    /// This work was derived from HelloWorld and extended to use the Mobile SDK for Windows Apps
    /// </summary>
    public partial class MobileHelloWorld : Form
    {
        private CmpApi cmpApi = null;
        private bool mobileDevice = false;

        /// <summary>
        /// Main constructor for MobileHelloWorld Form
        /// </summary>
        public MobileHelloWorld()
        {
            // initialize the designer standard components
            InitializeComponent();

            // initialize the Mobile SDK so we can use it
            InitializeMobileSDK();

            // change how the form looks based on what kind of device it is
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;
            }
            else
            {
                // make sure that this window is maximized on non-mobile devices
                WindowState = FormWindowState.Maximized;
            }
        }

The initialization code for the MobileHelloWorld code has been extended to call InitializeMobileSDK and to change the style of the form based on whether or not the device is mobile. The InitializeMobileSDK function is displayed later in this post and is responsible for creating an instance of the SDK object and hooking events. The MobileHelloWorld form is only created once and this related code is only run once. You only need to create the object for the SDK once so this is a good match. Note also that IsMobileDevice is used to decide between maximizing (non-mobile) and getting rid of the border (mobile).

        /// <summary>
        /// Initialize the Mobile SDK API object
        /// </summary>
        private void InitializeMobileSDK()
        {
            try
            {
                // create an instance of our API (referenced from Citrix.Cmp.dll assembly)
                cmpApi = new CmpApi();

                // hook the revelant events to support our app

                // viewport is the area we can use for our application
                cmpApi.Display.ViewportInfoChanged += new EventHandler<ViewportInfoChangedArgs>(Display_ViewportInfoChanged);

                // request notification for when we are connected and disconnected to properly handle the mobile device
                cmpApi.SessionManager.SessionConnected += new EventHandler<SessionConnectedArgs>(SessionManager_SessionConnected);
                cmpApi.SessionManager.SessionDisconnected += new EventHandler(SessionManager_SessionDisconnected);

                // the mobile device is only connected whe IsCmpAvailable is true
                if (cmpApi.SessionManager.IsCmpAvailable)
                {
                    mobileDevice = true;
                }
            }
            catch
            {
                // we did not get a working instance of CmpApi.
                // fail gracefully to allow it to work fine on non-mobile devices
            }
        }

This is InitializeMobileSDK(). The CmpApi object is used for everything in the SDK. In this code, an CmpApi object is created and stored in a cmpApi variable in the form class. Three event handlers are added to the object to be notified of viewport changes and session connect/disconnect. The viewport changes are important for making sure the application gets notified of viewport size changes (usually from screen keyboard or orientation change). The session connect/disconnect events help the application match the state of the device to the application. Beyond creating the CmpApi object, the function also makes sure that a mobile device is actually there. If it is, it then sets the mobileDevice flag which is used with IsMobileDevice function. If anything fails during this function call with an exception, the try/catch section will prevent the exception from being passed up. This is done to make the application function without exceptions if not connected to a mobile device.

        /// <summary>
        /// Session disconnected event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionDisconnected(object sender, EventArgs e)
        {
            // the mobile device is now no longer connected
            mobileDevice = false;
        }

        /// <summary>
        /// Notified when session is connected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionConnected(object sender, SessionConnectedArgs e)
        {
            // if the mobile device has returned, then allow mobile device calls again
            if (cmpApi.SessionManager.IsCmpAvailable)
            {
                mobileDevice = true;
            }

            // resize the form to match the current connected device
            ResizeForm();
        }

Session connect/disconnect events correspond to when the session is first connected to the device and when the session is disconnected. The disconnect event is only needed to know that the mobile device is no longer connected. The connect event is used to check for a mobile device and to resize the form to match the screen size.

        /// <summary>
        /// Viewport changed event - we are interested in the ClientViewport and use it to resize the main form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Display_ViewportInfoChanged(object sender, ViewportInfoChangedArgs e)
        {
            ViewportInfo viewport = e.NewState;

            if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0.0) && (viewport.ClientViewport.Value.Height != 0.0))
            {
                Size = new Size(Convert.ToInt32(viewport.ClientViewport.Value.Width), Convert.ToInt32(viewport.ClientViewport.Value.Height));

                Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
            }
            else
            {
                if (viewport.ClientViewport.HasValue)
                {
                    Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
                }
                else
                {
                    Trace.WriteLine("ViewportInfoChanged ClientViewport has no value");
                }
            }

            // if the server viewport has changed, make sure our app is at the origin
            if ((viewport.ServerViewport.HasValue) && ((viewport.ServerViewport.Value.X != 0) || (viewport.ServerViewport.Value.Y != 0)))
            {
                Location = new Point(Convert.ToInt32(viewport.ServerViewport.Value.X), Convert.ToInt32(viewport.ServerViewport.Value.Y));
            }
            else
            {
                Location = new Point();
            }
        }

The viewport event is very important for knowing the correct form size. The event has both client and server viewport information. The client viewport reveals the width and height of the area on the mobile device that can be used. The server viewport reveals what section of the server display is currently visible. Usually the two viewports match up. However, there are times when they do not. The most obvious cases are when the screen keyboard is displayed or when zooming is used. The event handler above is using tracing since viewport events can be a bit tricky to understand. I added the trace to understand why it was originally not doing what I wanted when using a keyboard. During the development of the SDK, it has been very helpful to understand the various events and what the values can be in different states. The event handler is using the client viewport to resize the form. It also uses the server viewport to reposition the form. Between these two sources of information, the form is correctly sized and positioned.

        /// <summary>
        /// Get the current display size for the mobile device.
        /// There are three techniques to attempt in a certain order.
        /// 1. Viewport information
        /// 2. Display state
        /// 3. Actual screen width and height
        /// The viewport is best since it takes into account keyboard and other mobile device areas which are reserved.
        /// The display state is from receiver but does not take into account keyboard or other reserved areas.
        /// The last is the real screen size which comes from Windows.
        /// </summary>
        /// <returns>Size of the display area</returns>
        private Size GetDisplaySize()
        {
            // start with the current size as the default
            Size size = Size;

            if (IsMobileDevice())
            {
                try
                {
                    ViewportInfo viewport = cmpApi.Display.GetViewport();

                    // ViewportInfo.ClientViewport is a nullable value and that means that we have to use HasValue to check it
                    // Also, the iOS Receiver sometimes reports 0,0 we need to check for that too
                    if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0) && (viewport.ClientViewport.Value.Height != 0))
                    {
                        size.Width = Convert.ToInt32(viewport.ClientViewport.Value.Width);
                        size.Height = Convert.ToInt32(viewport.ClientViewport.Value.Height);
                    }
                    else
                    {
                        // The display state resolution is a width, height measurement in pixels
                        DisplayState displayState = cmpApi.Display.GetDisplayState();

                        if (displayState.Resolution.HasValue)
                        {
                            // the display state is the next best place to go
                            size.Width = Convert.ToInt32(displayState.Resolution.Value.Height);
                            size.Height = Convert.ToInt32(displayState.Resolution.Value.Width);
                        }
                        else
                        {
                            // last resort is the screen width and height
                            size.Width = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenWidth);
                            size.Height = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenHeight);
                        }
                    }
                }
                catch
                {
                    // fail gracefully
                }
            }

            return (size);
        }

Being able to get the correct display size is very important to the application. In order to make this more fool-proof, the GetDisplaySize function uses three different techniques to get the information. In priority order, the different sources are used to find the right match. Having a fall-back makes sure that it will always get the best answer in different environments. It is also implemented to gracefully fail and not change the size if all these techniques fail. The main source will always be coming from viewport client viewport.

        /// <summary>
        /// Determines if mobile device is on the other side
        /// </summary>
        /// <returns>true - mobile device, false - non-mobile device</returns>
        private bool IsMobileDevice()
        {
            return (mobileDevice);
        }

Instead of querying the mobile device all the time, the status of the mobile device connection is kept in a boolean that is used in a function called IsMobileDevice.

        /// <summary>
        /// Called when the form is first loaded.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender">Ignored</param>
        /// <param name="e">Ignored</param>
        private void HelloWorld_Load(object sender, EventArgs e)
        {
            // resize and relocate based on mobile device informtation
            ResizeForm();
        }

        /// <summary>
        /// Resize the Form if it is running on mobile device
        /// </summary>
        private void ResizeForm()
        {
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;

                // Location is always (0,0)
                Location = new Point();

                // Size is calculated from the Mobile SDK information
                Size = GetDisplaySize();
            }
            else
            {
                WindowState = FormWindowState.Maximized;
            }
        }
        /// <summary>
        /// Called when the form is resized.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HelloWorld_Resize(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Resize the Hello World to fill the client area
        /// </summary>
        void ResizeHelloWorld()
        {
            // change the font to match the size of the client area
            HelloWorldLabel.Font = GetBestFontFit(ClientSize, HelloWorldLabel.Font, HelloWorldLabel.Text, ClientSize.Height, 8f);

            // center the label
            CenterHelloWorld();
        }

Whenever the form is first loaded or when the session is reconnected, it calls ResizeForm. The ResizeForm then uses GetDisplaySize to resize the actual form size. When the form is resized, it triggers the form resize event when then resizes the text and centers it. The chain of events makes sure that a form resize will lead to a text resize as well. The code for resizing the text is the same as before.

There are things that might help to reduce the number of lines being used to make it a mobile app but I do think that it is more honest to show what the details are. The MobileHelloWorld.cs source file is 343 lines on my development machine. Also keep in mind that the infrastructure of using the SDK is lightweight due to using the object model and the fact that it does not require much to use the API. The original HelloWorld example was 138 lines. That means about 200 lines were added to support a mobile device. Also keep in mind that this is not lines of code but rather actual text file lines include brackets and blank lines. Also keep in mind that I wrote the two examples on the same day and only postponed this example till today since I needed to do a blog post for it. In other words, it took longer to do the post than it did to modify the HelloWorld program to support a mobile device display.

If you would a copy of the source for the entire project, it is available on ShareFile. This time I did not include the binary since it might be considered untrustworthy by virus scanners. If you have any questions, please let me know using the comments.

Simple WinForms Hello World Example

HelloWorld

Not long ago, a member of the team thought it would be a good idea to show the simplest program possible using the Mobile SDK for Windows Apps.  This was an excellent idea since “Hello World” is commonly used to introduce developers to new concepts.  The history of “Hello World” comes from the early 1970s with the C language and has prevailed even though things have changed so much since then.

The first step is to provide you with a really basic version of “Hello World” using WinForms.  Get the code from here.  It is a really small download at around 25K.  The download is a full Visual Studio 2010 project and even includes the bin directory with the HelloWorld.exe binary.  The binary is not signed so if you do not trust it you can build your own copy.

The code that drives this app is short so I will include in full here.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace HelloWorld
{
    /// <summary>
    /// The HelloWorld form demonstrates how to display "Hello World" with the appropriate font based on the size of the client area.
    /// Later, this will be transformed to support the Mobile SDK for Windows Apps.
    /// </summary>
    public partial class HelloWorld : Form
    {
        /// <summary>
        /// Main constructor for HelloWorld Form
        /// </summary>
        public HelloWorld()
        {
            // initialize the designer standard components
            InitializeComponent();

            // make sure that this window is maximized
            WindowState = FormWindowState.Maximized;
        }

        /// <summary>
        /// Called when the form is first loaded.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender">Ignored</param>
        /// <param name="e">Ignored</param>
        private void HelloWorld_Load(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Called when the form is resized.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HelloWorld_Resize(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Resize the Hello World to fill the client area
        /// </summary>
        void ResizeHelloWorld()
        {
            // change the font to match the size of the client area
            HelloWorldLabel.Font = GetBestFontFit(ClientSize, HelloWorldLabel.Font, HelloWorldLabel.Text, ClientSize.Height, 8f); ;

            // center the label
            CenterHelloWorld();
        }

        /// <summary>
        /// Center the HelloWorldLabel in the client area of HelloWorld form
        /// </summary>
        void CenterHelloWorld()
        {
            HelloWorldLabel.Location = new Point((ClientSize.Width / 2) - (HelloWorldLabel.Width / 2), (ClientSize.Height / 2) - (HelloWorldLabel.Height / 2));
        }

        /// <summary>
        /// Determine the best font match for the size/text.  The result should be between the maximum and minimum height.
        /// The size is the area to fill.  The new font is based on the old font which is passed in.
        /// </summary>
        /// <returns>Font</returns>
        public static Font GetBestFontFit(Size size, Font currFont, String text, float fontMax, float fontMin)
        {
            float fontPixels = fontMax;
            float minfontPixels = fontMin;

            // find the corresponding font size for the textbox size
            Font font = currFont;

            // make sure that the text is not null or empty before attempting to fit it in the area.
            // if it is null or empty, the current font is used since it does not matter.
            if (!String.IsNullOrEmpty(text))
            {
                Size textsize;

                font = new Font(currFont.FontFamily, fontPixels, currFont.Style, GraphicsUnit.Pixel);

                textsize = TextRenderer.MeasureText(text, font);

                // Check to see if the font fits in the area and if not, find a smaller one
                while ((((textsize.Height) > size.Height) || ((textsize.Width) > size.Width)) && (fontPixels > minfontPixels))
                {
                    // try to speed up the process of finding the right font size by detecting how much it is too big by
                    if (textsize.Height > size.Height)
                    {
                        // reduce the fontPixels based on how far off the font size is
                        fontPixels -= (textsize.Height - size.Height);
                    }
                    else if (textsize.Width > size.Width)
                    {
                        int charPixelWidth = textsize.Width / text.Length;
                        int targetPixelWidth = size.Width / text.Length;

                        // if the characters are too wide, reduce the fontPixels by how different the result is from the desired width
                        if (charPixelWidth > targetPixelWidth)
                        {
                            fontPixels -= (charPixelWidth - targetPixelWidth);
                        }
                        else
                        {
                            fontPixels -= 1.0F;
                        }
                    }
                    else
                    {
                        fontPixels -= 1.0F;
                    }

                    // drop the old one since it did not work
                    font.Dispose();

                    // get a new font based on the smaller font pixels size
                    font = new Font(font.FontFamily, fontPixels, font.Style, GraphicsUnit.Pixel);

                    // recalculate the size based on the new font
                    textsize = TextRenderer.MeasureText(text, font);
                }
            }

            // when all done, return the font to use
            return (font);
        }

    }
}

The most complicated part is figuring out the proper font to use to fill the client area. This code was modified from the code that is being used for UserInfo from the previous posts. The idea is that it loops over attempts to find the correct font starting from the biggest size to the minimum size.

The end result is a program that always shows “Hello World” in its biggest font possible to fit in the client area. If you change the size of the window, the text will adjust to fill it again. This is a good introduction to the concept of having text be re-sized to fit a specified rectangle. The effect is quite interesting if you re-size it constantly and see how fast the text adjusts. It almost appears to be an animation.

The next step will be to introduce the Mobile SDK for Windows Apps. This is mostly for the sake of matching the size of the display and the orientation.

WinForms Fonts on Mobile Devices

One of the problems with running Windows programs on a mobile device is that unless the program is aware of the higher DPI, it will show the text in a very small font.  If it goes too far, the font becomes too hard to read.SamsungSIIIWithDesktop

The screen shot above is from my test Samsung Galaxy SIII.  The size you see is about the same as the size on the mobile device if you are using a Windows desktop to view this post.  The SIII specs specify that the pixel resolution is 1280×720.  This fairly close to a standard desktop on a PC but on a much smaller device.  On Windows, the standard dots per inch is 96 DPI.  On high end mobile devices (think retina display) it is often 300+ DPI.  By rule of thumb, text takes up around 9 times less area on a mobile device with high DPI than a standard Windows desktop.  This difference cannot be ignored.  Consequences include poor readability and not being able to select items like buttons, menus, and lists.

The obvious solution is to make all the fonts bigger.  With Windows Presentation Framework (WPF), it is fairly easy to change how the application looks.  Unfortunately, this is not as easy to accomplish in WinForms.

When looking at this problem, originally it seemed possible to convince WinForms that the DPI had changed.  However, this only works if the DPI for Windows itself changes.  The problem is that the remote device does not define the DPI for the Windows session.  It might be possible to support this but it would most likely take a major effort from Citrix and Microsoft to support a native DPI that matches the device.  There is another way.

Many months ago I worked on a simple WinForms sample that also prototyped a way to handle fonts.  The idea was to get the real DPI from the device and then determine the multiplier between the device and the desktop.  When the text controls are displayed, the new size is based on the normal size times the multiplier.  This was better than doing nothing but it seemed a bit complicated.  It was hard to track the original size after the size had changed.  Also, only focusing on scaling did not address the need to reflow the content when the orientation changed. The sample is located on the Docs web site if you are curious.  The class is called ScaleForm that supported doing this.  At this point, I would not recommend doing it this way.  Besides the issues, it does not use CMP.NET which is the preferred way of supporting C#/.Net.  It is using the COM Interop interface which is a bit more raw.  This sample was the basis of what was later learned during the building of the sample called User Info.

During the development of this new sample, a number of things became clear.

  1. The font needed to grow along with the control
  2. The size of the font was now limited by the area reserved for the text
  3. Since the text size cannot be predetermined for a given size, an API needs to be called to calculate the size.
  4. Having the font too big was just as bad as being too small.
  5. A range of possible sizes needed to be determined to restrict the font size.
  6. Needed a way to place the text based on a template layout.

Default text control sizes are going to get you no where.  Mobile devices always have a higher DPI and therefore the text is always going to shrink.  The real question becomes how much it should grow.

When I first started working on this sample, I made some assumptions that turned out to be wrong in the end.  I assumed that I could just specify text locations in a fixed way with a fixed size.  It seemed like a good idea to use the existing text control.  It seemed a bit more pure this way.  Later on it became obvious that the layout was suffering based on limitations of this assumption.  WinForms controls are not always well suited for resizing and repositioning for a mobile device.  It does not look as good as it should.  This is a reflection of the text controls since they were not intended to be used this way.  The end result is that the sample now uses a refactored control technique.  Values from existing controls are reformatted into panels with multiple text lines.  If the values change, the panels are redone.

Original format:

AddressonPC

New format:

AddressoniPhone

It is not possible to get this kind of clean layout without combining the content together.  In this case, the six text boxes are combined into three labels with one label per line.  It is a simpler result and it looks better too.  If you are wondering why it had six fields, this was a direct result of this information coming from Active Directory in six properties.

It would be much easier to point to the code than to describe it here.  However, it is not ready to be shared yet so that will have to wait.

Meanwhile, I will share sections of the code to give you an idea of how this works.

        /// <summary>
        /// Adjust the font to fit in the specified cell
        /// </summary>
        /// <param name="control"></param>
        /// <param name="size"></param>
        /// <param name="cell"></param>
        public static Size AdjustFont(Control control, Size size, CellLayout layout, String[] textArray, FontRange minmaxFontHeight)
        {
            Size textsize = new Size();
            Font font = null;
            float bestFit = GetBestFontFit(size, layout, textArray, minmaxFontHeight, out textsize, out font);

            if (!AreFontsSame(font, control.Font))
            {
                control.Font = font;
            }

            return (textsize);
        }

        /// <summary>
        /// Determine the best font match for the size/layout/textArray
        /// </summary>
        /// <param name="size"></param>
        /// <param name="layout"></param>
        /// <param name="textArray"></param>
        /// <param name="minmax"></param>
        /// <returns></returns>
        public static float GetBestFontFit(Size size, CellLayout layout, String[] textArray, FontRange minmax, out Size textsize, out Font font)
        {
            float fontPixels = minmax.MaxHeight;
            float minfontPixels = minmax.MinHeight;
            int borderSize = (layout.BorderStyle != BorderStyle.None) ? 4 : 0;

            // find the corresponding font size for the textbox size
            font = new Font(layout.FontFamily, fontPixels, layout.FontStyle, GraphicsUnit.Pixel);

            textsize = GetMaximumMeasuredText(textArray, font);

            // having the font match the control exactly does not allow for a border so we give it a buffer of four pixels
            while ((((textsize.Height + borderSize) > size.Height) || ((textsize.Width + borderSize) > size.Width)) && (fontPixels > minfontPixels))
            {
                fontPixels -= 1.0F;

                // drop the old one
                font.Dispose();

                font = new Font(font.FontFamily, fontPixels, font.Style, GraphicsUnit.Pixel);

                textsize = GetMaximumMeasuredText(textArray, font);
            }

            return (fontPixels);
        }

        /// <summary>
        /// Are the two specified fonts the same?
        /// </summary>
        /// <param name="font1"></param>
        /// <param name="font2"></param>
        /// <returns></returns>
        private static bool AreFontsSame(Font font1, Font font2)
        {
            bool SameFont = false;

            if ((font1.FontFamily.Name == font2.FontFamily.Name) && (font1.Size == font2.Size) && (font1.Style == font2.Style))
            {
                SameFont = true;
            }

            return (SameFont);
        }


        /// <summary>
        /// Determine the lines that have the biggest height and width
        /// </summary>
        /// <param name="listbox"></param>
        /// <returns></returns>
        public static Size GetMaximumMeasuredText(String[] textArray, Font font)
        {
            Size maxSize = new Size(0, 0);
            Size fontSize;

            foreach (String text in textArray)
            {
                String fontText = text;

                if (fontText == "")
                {
                    // cannot measure an empty string so we add in our own string to try.
                    fontText = "Mop";
                }

                fontSize = TextRenderer.MeasureText(fontText, font);

                if (fontSize.Height > maxSize.Height)
                {
                    maxSize.Height = fontSize.Height;
                }

                if (fontSize.Width > maxSize.Width)
                {
                    maxSize.Width = fontSize.Width;
                }
            }

            if ((maxSize.Height == 0) && (maxSize.Width == 0))
            {
                Trace.WriteLine("maxSize is (0,0) when it should not be set that.");
            }

            return (maxSize);
        }

In English, this code is fitting the text into the size of a cell. The cell size is an invented concept for containing a control. The text will fit into the control which fits inside the cell size determined by the layout. The minimum and maximum font heights are determined before calling this code. The cornerstone of this code is TextRenderer.MeasureText which can determine the size of the text specified without having to render it. This permits getting a perfect fit after a few attempts within the min/max range. The functions are setup to being able to get the maximum size of the text for multiple lines. This is ideal for fitting the content into the panel.

There is much more to explain about the layout and placement of the controls so that will be continued in the next post. However, it is important to explain the basics now. The model in the sample is to have a Layout and a Placement class. The Layout class is responsible for the arrangement of the controls in an abstract table format. It does not have any precise locations but is instead based on different measurements that will be resolved only in the Placement class.

The model is to use a row and cell system. Multiple cells can exist on one row. Rows are not limited but it is important to consider how things will be arranged on the screen at once. Each cell has a number of properties which control how the cell is used. The model evolved from trying to find something that would work well. It is not as comprehensive as Microsoft’s WPF support but it does address the basics of what WinForms needs to support a mobile device. The best part for me has been that I can modify the nature of how it works based on new requirements. The same will be true for you when you can get the download of the sample.

Here is the entire function that encapsulates the layout of the User Info program for a mobile device.

        /// <summary>
        /// Create the template which is used to place the controls later on
        /// </summary>
        private ControlTable GenerateUITemplate(ControlTableTemplate template)
        {
            ControlTable UserControlTable;
            FontFamily fontFamily = new FontFamily("Tahoma");
            double multiplier = GetDeviceMultiplier();


            CellMeasure ThumbnailMeasure = new CellMeasure(1.5 * multiplier, Units.Centimeter);
            CellSize ThumbnailSize = new CellSize(ThumbnailMeasure, ThumbnailMeasure);

            CellMeasure BioMeasureWidth = new CellMeasure(100, Units.Percent, Op.Subtract, ThumbnailMeasure);
            CellSize BioSize  = new CellSize(BioMeasureWidth, ThumbnailMeasure);

            CellMeasure IconMeasure = new CellMeasure(16 * multiplier, Units.Point);
            CellMeasure AddressMeasure = new CellMeasure(10 * multiplier, Units.Point);

            CellSize IconSize = new CellSize(IconMeasure, IconMeasure);

            CellMeasure ContactMeasureWidth = new CellMeasure(100, Units.Percent, Op.Subtract, IconMeasure);
            CellSize ContactSize = new CellSize(ContactMeasureWidth, IconMeasure);

            CellMeasure HeightMeasure = new CellMeasure(100, Units.Percent);
            CellMeasure WidthMeasure = new CellMeasure(100, Units.Percent);
            CellMeasure HalfWidthMeasure = new CellMeasure(50, Units.Percent);
            CellMeasure QuarterWidthMeasure = new CellMeasure(25, Units.Percent);
            CellMeasure BorderHeightMeasure = new CellMeasure(2, Units.Pixel);
            CellSize BorderBoxSize = new CellSize(WidthMeasure, BorderHeightMeasure);

            CellSize TextBoxSize = new CellSize(WidthMeasure, IconMeasure);

            CellMeasure AssociatesLineHeight = new CellMeasure(12 * multiplier, Units.Point);

            CellMeasure NonWorkAreaHeight = new CellMeasure(BorderHeightMeasure, Op.Add, IconMeasure, Op.Add, ThumbnailMeasure);
            CellMeasure WorkAreaHeight = new CellMeasure(HeightMeasure, Op.Subtract, NonWorkAreaHeight );
            CellSize WorkAreaBoxSize = new CellSize(WidthMeasure, WorkAreaHeight);
            LineSize AssociatesLineSize = new LineSize(WidthMeasure, AssociatesLineHeight);

            CellMeasure OneFifthWorkAreaHeight = new CellMeasure(WorkAreaHeight, Op.Divide, new CellMeasure(5.0, Units.Scalar));
            CellMeasure OneHalfWorkAreaHeight = new CellMeasure(WorkAreaHeight, Op.Divide, new CellMeasure(2.0, Units.Scalar));
            CellSize AddressPanelBoxMinSize = new CellSize(WidthMeasure, OneFifthWorkAreaHeight);
            CellSize AddressPanelBoxMaxSize = new CellSize(WidthMeasure, OneHalfWorkAreaHeight);

            CellMeasure BioLineHeight = new CellMeasure(ThumbnailMeasure.Measurement / 4.0, ThumbnailMeasure.Unit);
            LineSize BioLine = new LineSize(BioMeasureWidth, BioLineHeight);
            LineSize IconLine = new LineSize(IconSize);

            CellColors Transparent = new CellColors(this.ForeColor, this.BackColor);
            CellColors BlackWhite = new CellColors();
            CellColors BlackGrey = new CellColors(Color.Black, Color.WhiteSmoke);
            CellColors BlackBlack = new CellColors(Color.Black, Color.Black);
            CellColors BlackAntiqueWhite = new CellColors(Color.Black, Color.AntiqueWhite);
            CellColors BlackKhaki = new CellColors(Color.Black, Color.Khaki);
            CellColors BlackSilver = new CellColors(Color.Black, Color.Silver);
            CellColors BlackIndianRed = new CellColors(Color.Black, Color.IndianRed);


            CellPadding Padding4px = new CellPadding(4);
            CellPadding Padding2px = new CellPadding(2);
            CellPadding Padding3pxH = new CellPadding(3,0,3,0);
            CellPadding Padding1px = new CellPadding(1);
            CellPadding Padding0px = new CellPadding(0);

            CellLayout ThumbnailBox = new CellLayout(ThumbnailSize, BlackAntiqueWhite, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px);
            CellLayout BioBox = new CellLayout(BioSize, BioLine, BlackAntiqueWhite, fontFamily, FontStyle.Regular, BorderStyle.None, Padding3pxH);
            CellLayout ButtonBox = new CellLayout(IconSize, IconLine, BlackAntiqueWhite, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px);
            CellLayout ContactBox = new CellLayout(IconSize, Transparent, fontFamily, FontStyle.Regular, BorderStyle.None, Padding1px);
            CellLayout ContactTextBox = new CellLayout(ContactSize, Transparent, fontFamily, FontStyle.Regular, BorderStyle.None, Padding4px);
            CellLayout UserTextBox = new CellLayout(TextBoxSize, Transparent, fontFamily, FontStyle.Regular, BorderStyle.None, Padding4px);
            CellLayout AssociatesPanelBox = new CellLayout(WorkAreaBoxSize, AssociatesLineSize, BlackSilver, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px, CellSizing.Variable);
            CellLayout WorkAreaPanelBox = new CellLayout(WorkAreaBoxSize, AssociatesLineSize, Transparent, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px);
            CellLayout BorderBox = new CellLayout(BorderBoxSize, BlackBlack, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px);

            CellLayout AddressPanelTextBox = new CellLayout(AddressPanelBoxMaxSize, AssociatesLineSize, BlackKhaki, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px, CellSizing.Variable);

            CellLayout PhonePanelBox = new CellLayout(AddressPanelBoxMaxSize, AssociatesLineSize, BlackIndianRed, fontFamily, FontStyle.Regular, BorderStyle.None, Padding0px, CellSizing.Variable);

            UserControlTable = new ControlTable();

            if (template == ControlTableTemplate.TopWindow)
            {
                UserControlTable.AddRow(new ControlRow(RowNames.Thumbnail,
                                                       new ControlCell(CellNames.Thumbnail, Thumbnail, ThumbnailBox, CellHasData.Present, CellType.Image),
                                                       new ControlCell(new Control[] { FullName, Title, Department, Company }, BioBox, LayoutDirection.Vertical, CellHasData.Present, CellType.Multiple),
                                                       LayoutRelative.ContainerTopLeft,
                                                       CheckValue.Perform));

                UserControlTable.AddRow(new ControlRow(RowNames.Border, new ControlCell(CellNames.Border, BorderLine, BorderBox, CellHasData.None, CellType.Generic),
                                                       LayoutRelative.RowBelow,
                                                       CheckValue.Ignore));


                ControlCell.CalculateSizeHandler userDatasizeHandler = new ControlCell.CalculateSizeHandler(CalculateUserDataPanelSize);

                UserControlTable.AddRow(new ControlRow(RowNames.UserData,
                                                       new ControlCell(CellNames.UserData, UserDataPanel, WorkAreaPanelBox, CellHasData.Present, CellType.UserData, userDatasizeHandler),
                                                       LayoutRelative.RowBelow,
                                                       CheckValue.Ignore));


                ControlCell[] cells = new ControlCell[9];

                cells[0] = new ControlCell(CellNames.Home, HomeButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[1] = new ControlCell(CellNames.Back, BackButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[2] = new ControlCell(CellNames.Forward, ForwardButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[3] = new ControlCell(CellNames.Search, SearchButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[4] = new ControlCell(CellNames.Phone, WorkPhoneButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[5] = new ControlCell(CellNames.Address, AddressButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[6] = new ControlCell(CellNames.Associates, PeersButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[7] = new ControlCell(CellNames.Edit, EditButton, ButtonBox, CellHasData.None, CellType.Button);
                cells[8] = new ControlCell(CellNames.Off, OffButton, ButtonBox, CellHasData.None, CellType.Button);

                CellLayoutSettings buttonLayoutSettings = new CellLayoutSettings(LayoutRelative.ContainerBottomLeft, LayoutDirection.Horizontal, LayoutSpacing.EqualHorizontal, LayoutZOrder.Front);

                UserControlTable.AddRow(new ControlRow(RowNames.Buttons, cells, buttonLayoutSettings, CheckValue.Ignore));

                ControlGroup Header = new ControlGroup(GroupNames.Header, new String[] { RowNames.Thumbnail, RowNames.Border }, UserControlTable, GroupTypes.Header);
                ControlGroup Footer = new ControlGroup(GroupNames.Footer, new String[] { RowNames.Buttons },
                                                       UserControlTable, GroupTypes.Footer);

                UserControlTable.AddGroup(Header);
                UserControlTable.AddGroup(Footer);
            }
            else if (template == ControlTableTemplate.UserData)
            {

                // phone group

                ControlCell.CalculateSizeHandler phoneSizeHandler = new ControlCell.CalculateSizeHandler(CalculatePhonePanelSize);

                UserControlTable.AddRow(new ControlRow(RowNames.ContactDetails, 
                                                       new ControlCell(CellNames.PhonePanel, PhonePanel, PhonePanelBox, CellHasData.Present, CellType.Associates, phoneSizeHandler),
                                                       LayoutRelative.RowBelow,
                                                       CheckValue.Perform));

                UserControlTable.AddRow(new ControlRow(RowNames.Border2, new ControlCell(CellNames.Border, BorderLine2, BorderBox, CellHasData.None, CellType.Generic),
                                                    LayoutRelative.RowBelow,
                                                    CheckValue.Ignore));

                // address group
                ControlCell.CalculateSizeHandler addressSizeHandler = new ControlCell.CalculateSizeHandler(CalculateAddressPanelSize);

                UserControlTable.AddRow(new ControlRow(RowNames.FullAddress, new ControlCell(CellNames.AddressPanel, AddressPanel, AddressPanelTextBox, CellHasData.Present, CellType.FullAddress, addressSizeHandler),
                                                    LayoutRelative.RowBelow,
                                                    CheckValue.Perform));

                UserControlTable.AddRow(new ControlRow(RowNames.Border3, new ControlCell(CellNames.Border, BorderLine3, BorderBox, CellHasData.None, CellType.Generic),
                                                    LayoutRelative.RowBelow,
                                                    CheckValue.Ignore));

                // associates group
                ControlCell.CalculateSizeHandler associatesSizeHandler = new ControlCell.CalculateSizeHandler(CalculateAssociatesPanelSize);

                UserControlTable.AddRow(new ControlRow(RowNames.Associates, new ControlCell(CellNames.Associates, AssociatesPanel, AssociatesPanelBox, CellHasData.Present, CellType.Associates, associatesSizeHandler),
                                                    LayoutRelative.RowBelow,
                                                    CheckValue.Perform));

                UserControlTable.AddRow(new ControlRow(RowNames.Border4, new ControlCell(CellNames.Border, BorderLine4, BorderBox, CellHasData.None, CellType.Generic),
                                                    LayoutRelative.RowBelow,
                                                    CheckValue.Ignore));

                // put the rows in groups
                ControlGroup PhoneGroup = new ControlGroup(GroupNames.Phone, 
                                                           new String[] { RowNames.WorkPhone, RowNames.HomePhone, RowNames.MobilePhone, RowNames.Email, RowNames.Border2 },
                                                           UserControlTable, GroupTypes.Phone);

                ControlGroup Associates = new ControlGroup(GroupNames.Associates, 
                                                            new String[] { RowNames.Associates },
                                                            UserControlTable, GroupTypes.Associates);

                ControlGroup AddressGroup = new ControlGroup(GroupNames.Address, 
                                                             new String[] { RowNames.FullAddress, RowNames.Border3 },
                                                             UserControlTable, GroupTypes.Address);

                // add the groups into the table
                UserControlTable.AddGroup(PhoneGroup);
                UserControlTable.AddGroup(Associates);
                UserControlTable.AddGroup(AddressGroup);
            }
            else
            {
                Trace.Assert(false);
            }


            return (UserControlTable);
        }

Do not worry about the details too much. I just wanted to give you a taste of the layout code. It uses a number of other classes that support having rows and cells. It also refers to CellMeasure which is a class for specifying sizes in different units. Essentially it is a code version of something that could potentially could be done in HTML or XML. It is not as complicated as it first appears. Once you learn a row, then it is easy to do another.

Knowing this, it is also important to point out that this one function is generating two different control tables. The first one (Top window) is the outer window that contains the header and footer (user bio and buttons). In the middle, a scrollable region exists using a viewer and scrolling panel. The scrolling panel uses the second control table. In this table, it only has three panels which correspond to the contact details, the office information, and the related employees. The panels are attached to the scrollable panel based on the second control table. The size is determined for each panel by using a callback mechanism. The reason why it has a callback is that the content is not known during the generation of the template. Later on, the Placement class will formalize the sizes and use the callback. It allows the panels to be the right sizes based on the content.

Like mentioned in a previous post, the Layout/Placement code can support relocation of existing controls or refactoring them. Originally it was intended to relocate only. This was not practical. However, the relocation is still happening for some controls like the ones in the bio and the picture.

Even though there is more to describe, this is enough for this blog post. See you next time.