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.

Mobile Makeover – Before and After

UserInfoLast week I wrote a few different posts about converting an existing WinForms app into a mobile application.  What I forgot to do was show what my example application looked like before it was converted.  This blog will make up for that.  The above image was taken from a screen capture of the program running on Windows Server 2008 R2.  Note the blotched out areas.  This was done to hide the personal data while still providing a real live example.

What you see is fairly standard for a WinForms program.  There are a number of fields (usually TextBox controls) with support for a menu and title bar.  This particular app is a bit simplistic and even a bit clunky, but it gets the job done.

Different users can be selected from clicking their names (Manager, Peers, and Direct Reports).  Once selected, the information is loaded from Active Directory and displayed on the screen in the correct fields.

The icons are not really following the rules of a typical WinForms application.  It was hard to ignore that this app would eventually be a mobile app in some cases.  Instead of a standard button, pictures were used instead.  These pictures more closely represent the action desired.

Internally, the program is using features that require .Net 3.5 Runtime.  The main reason why is that Microsoft provided a better Active Directory collection of classes.  In this group, the one that UserInfo uses is UserPrincipal.  It is much more straightforward to use than the original DirectoryEntry class.  The result is that UserInfo would require .Net 3.5 which is installed as part of Windows 7 and above.

Even though this below has been included before in a previous post, it is valuable to include it here for contrast for before and after.

Portrait

Note that the information from the first picture is not missing.  It is just out of scroll range.  If the picture was taken either above or below, you would see the information that I had blocked out from the first one.  Also note that I added some features to the mobile version.  The two most obvious ones in this image are the date/time of the office location and the map.  Both of these were derived from Google API and were not that difficult to implement.  The map makes the app look a lot more interesting since it includes a scrollable image.

The goal was to make it possible to support both the old and the new from the same executable.  This was largely achieved by breaking out the mobile code and using conditions with IsMobile().

Everything starts from Main in the Program class like any other program.

namespace MobileUserInfo
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new UserInfoForm());
        }
    }
}

Notice that our main form is called UserInfoForm. It uses the same form for both the mobile and non-mobile cases. The difference is that the mobile code is very dynamic about how the information is displayed. When the UserInfoForm is created, it automatically calls the constructor for the class.

        //
        // UserInfoForm constructor
        //
        public UserInfoForm()
        {
            Init();
        }

        /// <summary>
        /// Constructor related routine for UserInfoForm class
        /// </summary>
        private void Init()
        {
            // intialize the standard WinForms controls
            InitializeComponent();

            // initialize mobile specific settings
            MobileInit();

            // userCommon is used for getting user information
            userCommon = new UserCommon(userCredentials, Resources.PropertySchema);

            // Validate the users current credentials.  If not in a domain, ask for username/password
            ValidateUserCredentials();

            // create the worker thread for async calls
            workerThread = new ADWorkerThread();
            workerThread.CreateWorkerThread();

            UserLoaded += new EventHandler(UserInfoForm_UserLoaded);
        }

The most relevant line is the one that calls MobileInit(). The MobileInit() function is a file separate from the main code path. In MobileInit(), it will check to see if it should initialize things based on being a mobile device.

        /// <summary>
        /// Called during constructor timeframe for UserInfoForm.
        /// Mobile related functions are executed here to simplify the 
        /// model of the main program.
        /// </summary>
        void MobileInit()
        {
            try
            {
                MobileAPI = new MobileAPI();

                if (IsMobile())
                {
                    // hook everything that we want to see
                    MobileAPI.HookOrientationStateChanged(OrientationChanged);
                    MobileAPI.HookViewportInfoChanged(ViewportChanged);
                    MobileAPI.HookSessionConnected(SessionConnected);
                    MobileAPI.HookSessionDisconnected(SessionDisconnected);
                    MobileAPI.HookTouchInputModeChanged(TouchInputModeChanged);
                    MobileAPI.HookDisplayStateChanged(DisplayStateChanged);

                    // enable the mouse raw feedback
                    MobileAPI.SetTouchInputMode(TouchInputMode.RawMouseInput);

                    // Gather together the various settings
                    AcquireMobileSettings();

                    if (ClientState != null)
                    {
                        Connected = (ClientState.ConnectionState == ConnectionState.Connected);
                    }

                    CityStatePostCode = CreateTextBox("CityStatePostCode", "");

                    UserPanel = CreatePanel("UserPanel", this, Size);
                    BorderLine = CreatePanel(RowNames.Border, null, null);
                    BorderLine2 = CreatePanel(RowNames.Border2, null, null);
                    BorderLine3 = CreatePanel(RowNames.Border3, null, null);
                    BorderLine4 = CreatePanel(RowNames.Border4, null, null);
                    PhonePanel = CreatePanel("PhonePanel", null, null);
                    AssociatesPanel = CreatePanel("AssociatesPanel", null, null);
                    AddressPanel = CreatePanel("AddressPanel", null, null);
                    StatusPanel = CreatePanel("StatusPanel", null, null);
                    UserDataPanel = CreatePanel("UserDataPanel", null, null);
                    UserScrollPanel = CreatePanel("UserScrollPanel", UserDataPanel, UserDataPanel.Size);

                    MouseMoveHandler = new MobileMouseHandler();

                    Application.AddMessageFilter(MouseMoveHandler);

                    adTimezone = new ADTimeZone(adLocation);
                    adMap = new ADMap(adLocation);
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
            }
        }

Since the code is longer, the code snippet has been collapsed by default. Click to expand the code. There is more information than what is needed but it does prove that MobileInit() is checking for IsMobile() before initializing everything. The MobileAPI is a wrapper class for the Mobile SDK for Windows Apps API. If the MobileAPI is created and connected to the device, it will allow IsMobile to work and the initialization code will run. It is also worth mentioning that the MobileInit() routine hooks six different events from the SDK. These events are important for making sure that the application is responsive to changes on the mobile device and the session. It is also interesting that MobileInit() is creating a number of Panels. These elements are unique to the mobile version of the UI and will be better explained later on.

The same model is used for when the form is first loaded. The main code runs and it gives the mobile version of code a chance to process the event.

        /// <summary>
        /// Any kind of Form initialization happens here
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void UserInfoForm_Load(object sender, EventArgs e)
        {

            if (Authenticated == false)
            {
                // do not go any further if not authenticated
                ExitApp();
            }
            else
            {

                try
                {
                    String imgFile = Properties.Settings.Default.BackgroundImage;

                    if (imgFile.Length > 0)
                    {
                        Image image = Image.FromFile(imgFile);

                        BackgroundImage = image;
                    }

                    this.Icon = Resources.usericon;

                    // adjust the size to fit the device
                    if (IsMobile())
                    {
                        AdjustFormSize();
                    }

                    // seed the UI with the current user information
                    ShowFirstUserDetails();
                }
                catch (Exception ex)
                {
                    ErrorDialog.Show(ex);
                    ExitApp();
                }
            }
        }

        /// <summary>
        /// Adjust the form to fit the mobile device
        /// Called from main form load event
        /// </summary>
        private void AdjustFormSize()
        {
            if (IsMobile())
            {
                // guarantee that we have all the mobile settings we need
                AcquireMobileSettings();

                // change the background color
                this.BackColor = Color.WhiteSmoke;

                // hide the menu
                menuStrip1.Visible = false;

                // hide the resize border and title bar
                // do this before our resizing since getting rid of border and caption will cause the window to resize down
                FormBorderStyle = FormBorderStyle.None;

                //Clear out our controls from the current group control
                UserInfoGroup.Controls.Clear();
                UserInfoGroup.Visible = false;

                // Adjust the size of the form and panel based on mobile settings
                PlaceForm();

                MouseMoveHandler.MouseMove += Form_MouseMove;
            }

        }

AdjustFormSize() is the function called upon when the form is first loaded. It only runs this function if it is a mobile device. A previous post has disclosed how to remove the title bar and get rid of the controls but they are shown here again in the above collapsed code section. PlaceForm is responsible for making sure the form fits the screen based on the API data.

This brings us a bit closer to understanding the basics of how this works. This blog has covered:

  • Before and After shot
  • Code path for mobile/non-mobile initialization
  • Some real code snips from UserInfo

The main goal was to illustrate what the UI looks like for non-mobile and mobile. The next blog will focus more on the issues brought up last week for converting the UI.

Citrix Mobile SDK for Windows Apps

toolkitIn recent posts, I have been talking about the Mobile SDK for Windows Apps and how to change Windows programs to use it.  The problem is that I have not recently covered what is included in the SDK and where to download it from.

download

The link is https://www.citrix.com/downloads/xenapp/components/mobile-sdk-for-windows-apps#ctx-dl-eula.

The SDK can be installed on Windows 7, Windows 8.  It was designed to make it easier for developers to build and test their applications before deploying them onto either XenApp or XenDesktop.

Inside the SDK, there are several types of tools.

  • Samples
  • Includes
  • Libraries
  • Run-time
  • Documentation
  • Emulator

SDKDir

The most recent addition is the Emulator.  When the emulator is active, the Windows environment will be able to support virtual features that usually are only available when connected to a mobile device.  This reduces the complexity of building and testing applications.  The emulator simulates not only the mobile device but also the server code that normally supports the application.  For the most part, the emulator is transparent.  It being there is usually all you need.

Once the SDK is installed, the emulator will automatically run an application uses the Mobile SDK for Windows Apps.  This aspect is very useful because application execution guarantees that the right environment is present.  The emulator does not need to be changed to get things going but it does have some features that make it very useful.

Emulator

The two aspects that I use the most is changing the device type and changing the orientation.  Changing the device type allows for different form factors.  In my case, I typically switch between iPhone and Galaxy Tab 10.1.  By doing this switch between application execution, I can try the application in both phone and tablet layout.  As for the orientation, it makes it easy to test both the portrait and landscape layout.  All that needs to happen is pushing the orientation you want.  The app will change size and layout based on this.

Over the last few months I have been working on building a sample using WinForms.  It became an exercise in using our own software and tools.  The emulator allowed me to do most of the work on Windows 7.  The iteration cycle was very fast and the results were immediate.  It would have taken much longer to involve a mobile device and server.  The most impressive aspect was how solid it has been.  It is a tool that enables quicker development.  It also means that it can be done without using a real mobile device.

It is worth mentioning that the SDK includes samples that could form the basis of an application.  Under the samples directory, the Demo.WPF and the XaAdminMobile applications are worth looking at in detail.  Demo.WPF covers the feature set of the SDK.  XaAdminMobile is a real tool which can be used to manage XenApp users.  There is nothing wrong with taking these samples and modifying them for what you need.

This is a quick summary of the SDK and hopefully there is something new here for you.  I would recommend downloading it and giving it a try.  There is no cost but you will need to have a Citrix web site login to get it.  Good luck and thanks for your time.

Remodeling a WinForms App

whitehouserenovation

Everything needs to be updated eventually.  What I hope to share is how to update a WinForms application to support mobile devices. This post will cover the basics of the things to be concerned about.

  • Size and position of forms and controls
  • Orientation
  • Fonts
  • Buttons
  • Non-mobile friendly controls

Size and Position

The first concept to understand that unlike standard desktops, mobile devices come in many different sizes.  And, the size can change at any time based on changing the orientation or displaying the on-screen keyboard.  Assuming a fixed size and layout is a bad thing to do.  The application needs to adjust based on the display changes.  Not only does the Form need to be re-sized, but the content needs a new layout.  This step can be a bit tricky for a standard Windows Forms application.

Landscape

Landscape orientation for WinForms mobilized application

Portrait

Portrait orientation for WinForms mobilized application

In general, this occurs during application start up and during orientation changed events. The application needs to gather the information and then apply that information against the container (form) and content (controls).  It also needs to make sure that it gets updates when they happen (events).

Since we are using WinForms, we are using the CMP.NET component from the SDK to access the information and hook the events.  CMP.NET is contained in the assembly Citrix.CMP.dll and is located in the same directory as the application.  It is important to remember that some files of the Mobile SDK for Windows Apps are allowed to be distributed with the applications that use them.  The namespace is Citrix.CMP and the documentation is available online at Citrix.CMP docs site.

The typical location for the Citrix.CMP.dll assembly is:

C:\Program Files (x86)\Citrix\MobilitySDK\bin\AnyCPU

In order to use it with your WinForms program, you need to reference it from the Visual Studio project. You also should include a using reference to simplify the naming.

using Citrix.Cmp;

In order for the interface to be used, it needs to be initialized.  Overall, this just means creating a new object.

///
/// CMP.NET object for Citrix Mobility Pack
///
CmpApi cmpApi = null;

// create the CMP.NET object
cmpApi = new CmpApi();

To test if CMP.NET is actually available, you need to call IsCmpAvailable from the SessionManager. The CmpApi object can be created when there is no connection to Citrix Receiver. This can happen either when the session is disconnected or the Receiver does not support the mobile extensions.

///
/// Internal flag to reveal if SDK is available
///bool cmpAvail = false;

// confirm that the Citrix Mobility Pack is active
if (cmpApi.SessionManager.IsCmpAvailable == true)
{
    cmpAvail = true;
}
else
{
    cmpAvail = false;
}

This can be wrapped in a simple function

///
/// Determines if we have a mobile device available
///
/// bool - true(mobile) false(non-mobile)
public bool IsMobile()
{
    return (cmpAvail);
}

This IsMobile() function can be widely used to determine application behavior. For example, the layout can be determined based on this result. It can also be used to safely ignore mobile-related requests for functions trying to use the SDK.

///
/// Get the current orientation state
///
/// OrientationState
public OrientationState GetOrientationState()
{
    OrientationState state = null;

    if (IsMobile())
    {
         state = cmpApi.Display.GetOrientationState();
    }

    return (state);
}

You only need to get one instance of CmpApi for an application. Also, you should only create the object on the main UI thread to guarantee event notification. If you do not do this, there is a good chance that you will not receive events you want. In the Mobile User Info program example, I created a class just to manage the interfaces with the SDK. I did this for a few reasons but the main reason was to better control what was used from the interface. Instead of changing many different places related to the SDK, having one main class makes it easier to manage and standardize. It is not a requirement to do this but it can make things more friendly.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Citrix.Cmp;
using System.Diagnostics;

namespace MobileUserInfo
{
    /// <summary>
    /// Wrapper class for mobility calls.  The intent is to reduce what is available to what is used
    /// and also simplify to a small degree.
    /// </summary>
    public class MobileAPI
    {
        /// <summary>
        /// CMP.NET object for Citrix Mobility Pack
        /// </summary>
        private CmpApi cmpApi = null;

        /// <summary>
        /// Internal flag to reveal if SDK is available
        /// </summary>
        private bool cmpAvail = false;

        /// <summary>
        /// Constructor for MobileAPI object
        /// </summary>
        public MobileAPI()
        {
            // create the CMP.NET object
            cmpApi = new CmpApi();

            // confirm that the Citrix Mobility Pack is active
            if (cmpApi.SessionManager.IsCmpAvailable == true)
            {
                cmpAvail = true;
            }
            else
            {
                cmpAvail = false;
            }
        }

        /// <summary>
        /// Determines if we have a mobile device available
        /// </summary>
        /// <returns>bool - true(mobile) false(non-mobile)</returns>
        public bool IsMobile()
        {
            return (cmpAvail);
        }

This is only a small part of what the class contains. To see more, you will have to wait for the sample to be available.

Returning to the topic of the form size and position, let’s use some SDK APIs to get what we want. There are a couple of concepts to explain. Their are two main ways to determine the size of the mobile screen. The first is GetDisplayState. The second technique is to use GetViewportInfo. The difference between the two is that GetViewportInfo is more descriptive of the area that the application can use. GetDisplayState just returns the entire screen size. The viewport information contains: ServerViewport, ClientViewport, and ZoomFactor.

Viewport information
Viewport item Type Description
Client viewport Rectangle Area available on mobile device
Server viewport Rectangle Server area currently displayed
Zoom factor Integer Current zoom factor (default=100 no zoom)

The field that we want is Client viewport. With this information, we can set the size of the form. The client viewport origin is always (0,0). The width and height vary based on device size, orientation, and keyboard popup. To gather the current viewport information, call GetViewport.

/// <summary>
/// Get the current viewport settings
/// </summary>
/// <returns>ViewportInfo</returns>
public ViewportInfo GetViewport()
{
    ViewportInfo state = null;

    if (IsMobile())
    {
        state = cmpApi.Display.GetViewport();
    }

    return (state);
}

To apply this to the form, simply change the Form’s size.

/// <summary>
/// Place the form at the origin and correct size for mobile device
/// </summary>
private void PlaceForm()
{
    ViewportInfo Viewport = MobileAPI.GetViewport();

    if ((Viewport != null) && (Viewport.ClientViewport.HasValue))
    {
        Size = new Size((int)Viewport.ClientViewport.Value.Width, (int)Viewport.ClientViewport.Value.Height);
    }

    // make sure the form is at 0,0 on screen
    Location = new Point(0, 0);
}

This is the approach to use when the form is created but we also need to know when the client viewport changes. There is an event for viewport changes that can be hooked. It is called ViewportInfoChanged. The event provides the new viewport information in the arguments.

/// <summary>
/// Hook into the ViewportInfoChanged
/// </summary>
/// <param name="viewportEventHandler">ViewportInfoChanged Event handler</param>
public void HookViewportInfoChanged(EventHandler<ViewportInfoChangedArgs> viewportEventHandler)
{
    if (IsMobile())
    {
        cmpApi.Display.ViewportInfoChanged += viewportEventHandler;
    }
}

Form init
...
    MobileAPI.HookViewportInfoChanged(ViewportChanged);
...

//
// whenever the viewport changes, we need to re-adjust our form
//
private void ViewportChanged(object sender, ViewportInfoChangedArgs args)
{
    // if it has changed, then we need to re-layout our fields
    Viewport = args.NewState;

    ReflowForm();
}

If the viewport changes, we will know and the form will be resized

When it comes to the controls, something new needs to happen. Either the controls need to be moved and resized or we need to create a new way of displaying content. Originally when I was investigating the best way to handle this, it seemed like using the real control was the right direction. Over time, this changed to preferring techniques of refactoring. The primary reason why is that by refactoring the information, it gave a better result. The end result is a collection of code that can handle either approach.

Given that I have been working on this post on and off for the last three days, I am going to continue in the next post for the more detailed information.

Did you notice the remodeling of the White House? It is the picture at the top and it links to the history of this serious renovation between 1948 and 1952. A few of the pictures reveal that they gutted the White House. I promise that you will not have to gut your WinForms application to play nicely on mobile devices. The idea here is to make things as easy as possible.

In this post, we have covered

  • Where to get CMP.NET
  • How to include CMP.NET
  • How to get an CMP object
  • Viewport is explained
  • Viewport event hooking
  • Form resizing
  • Concept of mobile and non-mobile code

Not as far as I planned for this post but a great start. The next post should arrive in a couple of days.

Converting Windows Apps into Mobile Apps

window-view

The story of Microsoft Windows began in 1985.  A young Microsoft realized it needed a GUI instead of just DOS.  The Microsoft history tends to ignore the influence of the market.  At the time, Apple had released Macintosh and were a serious competitor to DOS.

Continue reading