Skip to main content Skip to footer

Embedding .NET Controls in Windows Vista Sidebar Gadgets

Applies To:

Studio Enterprise

Author:

John Juback

Published On:

4/11/2007

Sidebar gadgets are a new feature of Windows Vista that allows users to run mini HTML-based applications on the desktop. Although a typical gadget uses dynamic HTML with lots of script code, it is also possible for gadgets to host .NET assemblies derived from UserControl, providing a richer environment for both gadget developers and end users.

This article provides an in-depth look at a gadget that uses two Studio Enterprise controls, C1FlexGrid and C1SuperLabel, to implement a version of the popular Sudoku puzzle. It also details the administrative steps needed to prepare such gadgets for deployment and installation on client machines.

To download the source code (in C#) for the C1Sudoku gadget, visit the following link:

[http://helpcentral.componentone.com/c1kb/upload/C1SudokuGadgetProject.zip](//cdn.mescius.io/assets/developer/blogs/legacy/c1/2007/4/C1SudokuGadgetProject.zip)

The download link and installation instructions for the gadget itself are given at the end of this article.

We begin by creating a new Windows Control Library project in Visual Studio 2005. Rename the default UserControl class (UserControl1.cs) to Gadget.cs. Switch to code view and add the following statement:

using System.Runtime.InteropServices;

This will enable the attributes that will allow our UserControl to serve as a COM object. Next, preface the class declaration with the ComVisible and Guid attributes:

[ComVisible(true), Guid("298376B6-61B1-4628-A408-725563FB389D")]  
public partial class Gadget : UserControl

For your own projects, substitute a unique GUID by running the Create GUID command on the Tools menu in Visual Studio. If your assembly will contain multiple UserControl subclasses, then each must have a different GUID.

In order for a UserControl assembly to be used in a Sidebar gadget, it must be strongly signed. In Project Properties, click the Signing tab, then check the box labeled Sign the assembly. In the combo box labeled Choose a strong name key file, select , then enter the name of the key file (with or without the .snk extension) and an optional password.

Adding Studio Enterprise Components

Now that we have a framework for a UserControl that can be hosted in a Sidebar gadget, we can add components to the design surface. This example uses the C1FlexGrid control for the Sudoku puzzle grid and a C1SuperLabel control for the command bar at the bottom.

The size of the Gadget UserControl is set to 127 pixels wide by 176 pixels high. The maximum suggested width for a gadget is 130; the value 127 was chosen to keep the widths of all cells equal.

The following properties were set on the C1FlexGrid control at design time:

AllowEditing

False

BorderStyle

None

Cols.Count

9

Cols.Fixed

0

Cols.MaxSize

14

Dock

Top

DrawMode

OwnerDraw

FocusRect

Solid

Rows.Count

9

Rows.Fixed

0

Rows.MaxSize

18

ScrollBars

None

SelectionMode

Cell

C1SuperLabel is similar to the standard Label control except that it has a lightweight HTML rendering engine that does not rely on Internet Explorer. In this example, C1SuperLabel is used to render status messages and command links at the bottom of the composite control. The following properties were set on the C1SuperLabel control at design time:

Dock

Fill

Font

Segoe UI, 8.25 pt, Bold

Text

Next

Rendering Cells in C1FlexGrid

The CellStyle objects used to render grid cells are created once in code. A separate style is used for each of the nine 3 x 3 grids (CustomStyle1 through CustomStyle9). Another nine styles are derived from these (with the string "_Filled" appended to the name) for rendering cells that have numbers entered by the user.

private Color[] customColors =  
{  
    Color.Transparent,  
    Color.Khaki,  
    Color.LightBlue,  
    Color.NavajoWhite,  
    Color.Thistle,  
    Color.PaleGreen,  
    Color.Lavender,  
    Color.Pink,  
    Color.Gainsboro,  
    Color.MistyRose  
};  

private void CreateStyles()  
{  
    for (int i = 1; i <= 9; i  )  
    {  
        string name = "CustomStyle"   i.ToString();  
        CellStyle cs = c1FlexGrid1.Styles.Add(name);  
        cs.BackColor = customColors[i];  
        cs.TextAlign = TextAlignEnum.CenterCenter;  

        CellStyle fs = c1FlexGrid1.Styles.Add(name   "_Filled", name);  
        fs.ForeColor = Color.Gray;  
    }  
}

Using CellRange objects, the custom styles are applied each time a new puzzle is first displayed.

private void ResetStyles()  
{  
    for (int i = 0; i < 3; i  )  
    {  
        for (int j = 0; j < 3; j  )  
        {  
            int k = (i * 3)   j   1;  
            CellRange rg = c1FlexGrid1.GetCellRange(i * 3, j * 3, (i * 3)   2, (j * 3)   2);  
            rg.Style = c1FlexGrid1.Styles["CustomStyle"   k.ToString()];  
        }  
    }  
}

Since the DrawMode property is set to OwnerDraw, the OwnerDrawCell event fires whenever the grid needs to render a cell. This is how the gradient fills were achieved for individual cells.

private void c1FlexGrid1_OwnerDrawCell(object sender, OwnerDrawCellEventArgs e)  
{  
    LinearGradientBrush b = new LinearGradientBrush(e.Bounds, Color.White, e.Style.BackColor, 90);  
    e.Graphics.FillRectangle(b, e.Bounds);  
    e.DrawCell(DrawCellFlags.Content | DrawCellFlags.Border);  
    e.Handled = true;  
}

Displaying Puzzle Data

Puzzles are stored as embedded text file resources (Puzzle1 through Puzzle4) having the following format:

_,5,7,_,_,_,2,_,_  
_,1,4,_,3,_,6,_,_  
_,_,_,7,_,1,_,3,_  
_,9,_,_,_,_,_,_,_  
_,2,8,4,_,9,5,6,_  
_,_,_,_,_,_,_,7,_  
_,8,_,2,_,4,_,_,_  
_,_,9,_,7,_,3,1,_  
_,_,5,_,_,_,9,8,_

Underscores represent blank cells, and commas are delimiters between cells. The following private member loads the specified string resource and converts it to an array of 81 single-character strings (either a number from 1-9 or a space). The CellRange object is used again, this time to set the underlying data instead of a style:

private void FillPuzzle(int n)  
{  
    string name = String.Format("Puzzle{0}", n);  
    string puzzle = C1SudokuGadget.Properties.Resources.ResourceManager.GetString(name);  
    puzzle = puzzle.Replace("\\r\\n", ",");  
    puzzle = puzzle.Replace("_", " ");  
    string[] numbers = puzzle.Split(",".ToCharArray());  

    c1FlexGrid1.Clear();  
    c1FlexGrid1.Tag = n;  
    ResetStyles();  

    for (int i = 0; i < 9; i  )  
    {  
        for (int j = 0; j < 9; j  )  
        {  
            string val = numbers[(i * 9)   j].Trim();  
            CellRange rg = c1FlexGrid1.GetCellRange(i, j);  
            rg.Data = val;  

            if (val.Length > 0)  
                rg.UserData = 1;  
        }  
    }  
}

Note the use of the UserData property. This is how the gadget will determine that a cell is part of the puzzle and therefore cannot be edited by the user.

Analyzing Cell Values

Sudoku has only one rule: Every row, every column, and every 3 x 3 grid must contain each of the numbers from 1 through 9. The gadget enforces this rule and provides hints to the player by scanning the cells that constrain the current cell. Whenever the user navigates to a different grid cell, the AfterRowColChange event fires:

private void c1FlexGrid1_AfterRowColChange(object sender, RangeEventArgs e)  
{  
    DescribeCell(e.NewRange.TopRow, e.NewRange.LeftCol);  
}

The private member DescribeCell determines the state of the new current cell and updates the text of the C1SuperLabel control accordingly:

private void DescribeCell(int row, int col)  
{  
    if (IsSolved())  
    {  
        c1SuperLabel1.Text = "Puzzle solved!  Next";  
        return;  
    }  

    CellRange rg = c1FlexGrid1.GetCellRange(row, col);  

    if (rg.UserData != null)  
        c1SuperLabel1.Text = "Click an open cell";  

    else if (rg.StyleDisplay.Name.EndsWith("_Filled"))  
        c1SuperLabel1.Text = "Erase";  

    else if (!GetAvailableValues(row, col))  
        c1SuperLabel1.Text = "Error detected";  
}

If the entire puzzle has been completed, a hyperlink for switching to the next puzzle is displayed. If the cell is not editable (UserData was set previously), then a message is displayed. If the name of the underlying style indicates that the user has already filled in the cell, then a hyperlink for erasing that cell is displayed. For empty cells, the private member GetAvailableValues takes care of building the hyperlinks representing the allowable values for the current cell. If that fails, then the user has made a mistake and an error message is displayed.

GetAvailableValues checks three cell ranges corresponding to the current row, column, and 3 x 3 grid, which are passed along with an array of booleans to VerifyCellRange, which examines the underlying data:

private bool GetAvailableValues(int row, int col)  
{  
    bool[] used = new bool[10];  

    int sqrow = row - (row % 3);  
    int sqcol = col - (col % 3);  

    VerifyCellRange(c1FlexGrid1.GetCellRange(row, 0, row, 8), used);  
    VerifyCellRange(c1FlexGrid1.GetCellRange(0, col, 8, col), used);  
    VerifyCellRange(c1FlexGrid1.GetCellRange(sqrow, sqcol, sqrow   2, sqcol   2), used);  

    StringBuilder sb = new StringBuilder();  

    for (int i = 1; i <= 9; i  )  
    {  
        if (!used[i])  
        {  
            sb.AppendFormat(" <a href="{0}">{0}</a>", i.ToString());  
        }  
    }  

    if (sb.Length > 0)  
    {  
        CellRange rg = c1FlexGrid1.GetCellRange(row, col);  
        c1SuperLabel1.Text = "Open: "   sb.ToString();  
        return true;  
    }  

    return false;  
}  

private void VerifyCellRange(CellRange rg, bool[] used)  
{  
    for (int row = rg.TopRow; row <= rg.BottomRow; row  )  
    {  
        for (int col = rg.LeftCol; col <= rg.RightCol; col  )  
        {  
            string s = c1FlexGrid1[row, col].ToString();  

            if (s.Length > 0)  
            {  
                int n = System.Convert.ToInt32(s);  
                used[n] = true;  
            }  
        }  
    }  
}

The IsSolved member called from DescribeCell does not perform a rigorous check of the solution, but merely checks that every cell has a number from 1 through 9:

private bool IsSolved()  
{  
    for (int i = 0; i < 9; i  )  
    {  
        for (int j = 0; j < 9; j  )  
        {  
            string val = c1FlexGrid1[i, j].ToString();  

            if (val.Length == 0 || !"123456789".Contains(val))  
                return false;  
        }  
    }  

    return true;  
}

This is sufficient because the gadget's event handlers validate the user's input on-the-fly.

Handling User Input

Even though the grid has AllowEditing set to False, we can still process keystrokes and take the appropriate action. The KeyPress event handler sets the value of the current cell if the key represents a number from 1 through 9. If the Backspace key is pressed, the current cell is cleared:

private void c1FlexGrid1_KeyPress(object sender, KeyPressEventArgs e)  
{  
    if (e.KeyChar >= 49 && e.KeyChar <= 57) // 1-9  
    {  
        string val = ((int)e.KeyChar - 48).ToString();  
        SetCurrentCell(val);  
    }  
    else if (e.KeyChar == 8) // Backspace  
    {  
        SetCurrentCell("");  
    }  

    e.Handled = true;  
}

The KeyPress event does not fire for the Delete key, so we handle the KeyDown event and clear the current cell:

private void c1FlexGrid1_KeyDown(object sender, KeyEventArgs e)  
{  
    if (e.KeyCode == Keys.Delete)  
    {  
        SetCurrentCell("");  
        e.Handled = true;  
    }  
}

The actual work is done by SetCurrentCell (but only if UserData is not set):

private void SetCurrentCell(string val)  
{  
    CellRange rg = c1FlexGrid1.CursorCell;  

    if (rg.UserData == null)  
    {  
        string name = rg.StyleDisplay.Name;  
        bool filled = name.EndsWith("_Filled");  
        rg.Data = val;  

        if (val.Length == 0 && filled)  
            rg.Style = c1FlexGrid1.Styles[name.Replace("_Filled", "")];  

        else if (!filled)  
            rg.Style = c1FlexGrid1.Styles[name   "_Filled"];  

        DescribeCell(c1FlexGrid1.Row, c1FlexGrid1.Col);  
    }  
}

Similarly, the LinkClicked event for C1SuperLabel is called to change the value of the current cell. For numeric links, e.HRef is a single-digit string. The sentinel value # is handled the same as Delete or Backspace. Links named Next cycle through the four puzzles stored as embedded string resources:

private void c1SuperLabel1_LinkClicked(object sender, C1SuperLabelLinkClickedEventArgs e)  
{  
    if (e.HRef == "#")  
        SetCurrentCell("");  

    else if (e.HRef != "Next")  
        SetCurrentCell(e.HRef);  

    else  
    {  
        int n = (int)c1FlexGrid1.Tag;  
        n = (n == 4) ? 1 : n   1;  
        FillPuzzle(n);  
    }  
}

At a minimum, a Sidebar gadget consists of an XML manifest file named gadget.xml and an HTML file that specifies the gadget's content. The XML manifest file is as follows:

gadget.xml



    C1 Sudoku  
    C1.Gadget.Sudoku  
    1.0.0.0  




    © 2007  
    Demonstrates how to embed .NET controls (C1FlexGrid and C1SuperLabel) in a gadget.  







            Full  





The HTML content file is basically a placeholder for the UserControl. Note that the classid attribute in the tag uses the same GUID as the attribute on the class declaration in Gadget.cs.

C1SudokuGadget.html



    C1Sudoku Gadget  

     body { width:129px; height:180px; margin:0; padding:2; background-color:gray; }  













Distribution and Installation

Sidebar gadgets are distributed as zip files with a .gadget extension. C1Sudoku.gadget contains the following files:

gadget.xml

Gadget manifest

C1SudokuGadget.html

Gadget content page

C1SudokuGadget.dll

UserControl assembly

C1.Win.C1FlexGrid.2.dll

C1FlexGrid assembly

C1.Win.C1SuperTooltip.2.dll

C1SuperLabel assembly

c1.gif

ComponentOne logo for the Gadget Gallery

icon.png

Icon for the Gadget Gallery

drag.png

Image shown when dragging the Gadget Gallery icon to the Sidebar

register.cmd

Command line for registering the UserControl as a COM object

To install the C1 Sudoku gadget, visit the following link:

[http://helpcentral.componentone.com/gadgets/C1Sudoku.gadget](http://helpcentral.componentone.com/gadgets/C1Sudoku.gadget)

Save the file to the desktop and then double-click its icon. In the Security Warning dialog, click Install. Right-click the Sidebar and select Add Gadgets to verify that the gadget was installed.

The gadget is displayed in the Sidebar as a gray rectangle with a red X. This is because the UserControl assembly has not yet been registered as a COM object. To perform the registration, open a Command Prompt using Run as administrator, then change the current directory to the following:

%userprofile%\\AppData\\Local\\Microsoft\\Windows Sidebar\\Gadgets\\C1Sudoku.gadget

Next, execute the following command line:

register.cmd

This command file calls the RegAsm utility as follows:

%windir%\\Microsoft.NET\\Framework\\v2.0.50727\\RegAsm C1SudokuGadget.dll /codebase

The only significant drawback to embedding .NET controls within a gadget is that the registration requires administrator privileges and a few extra steps. An alternative delivery method would be to create a Windows Installer file (.msi) with a digital signature. Although this would still require administrator privileges, it would eliminate security warnings and the need to open a Command Prompt.

You can use the techniques outlined in this article to implement your own Sidebar gadgets using any intrinsic or third-party .NET components, giving you access to a wide range of functionality not possible with HTML and script alone. During development, you can also use the UserControl Test Container in Visual Studio to refine your gadget's UI before deploying it to Windows Sidebar.

For more information on gadget development, see the Gadget Corner blogs on MSDN:

[http://blogs.msdn.com/sidebar](http://blogs.msdn.com/sidebar)

MESCIUS inc.

comments powered by Disqus