This is the fourth part in a series of tutorials aimed at guiding novice developers though building a "Hello World" Windows Home Server Add-In, using only freely-available tools (including Visual Studio Express).

In Part 4, we'll clean up the GUI to make it look a bit more shiny, and add our share permission change notification functions.

For this tutorial, we're building a Windows Home Server Add-In that detects changes to Shared Folder permissions. You can access the other parts of the tutorial, or download the code I'm using in the screenshots, using the links below.

  • Part 1: Installing and configuring your development environment
  • Part 2: Designing a GUI and initial deployment test
  • Part 3: Responding to Windows Home Server Notifications (code)
  • Part 4: Using FancyListView and comparing permissions (code)
  • Part 5: Building an installer package using WiX (code)

Note: The code samples below don't copy and paste very well directly into Visual Studio. If you don't want to type them out by hand, copy the sample into WordPad, and then copy from WordPad into Visual Studio. 

 

Step 1: Create a new object

As we talked about briefly last time, we're building an Add-In that will alert us when permissions are changed for our shared folders.

In order to compare the current shared folder permissions with some arbitrary known-good state, we need a way to save the permissions of all our shared folders to disk. That way, when the WHS Console is opened, we can retrieve our known-good state and compare it to the current set of permissions.

We're going to need a new object to represent our saved share permissions. Right-click our project and choose Add Class.

113addlist

Add the new class as SharePermissions.cs.

 114addlist

A new file will open in Visual Studio; this is our object. Change the generated code to look like this:

using System.Collections.Generic;
using Microsoft.HomeServer.SDK.Interop.v1;
 
namespace Microsoft.HomeServer.HomeServerConsoleTab.My_First_Add_In
{
    public class SharePermissions
    {
        private string shareName;
        private List<WHSUserPermission> permissions;
 
        public SharePermissions()
        {
            permissions = new List<WHSUserPermission>();
        }
 
        public string ShareName
        {
            get { return shareName; }
            set { shareName = value; }
        }
 
        public List<WHSUserPermission> Permissions
        {
            get { return permissions; }
            set { permissions = value; }
        }
    }
}

 

We've now created an object that can hold a share name, and a list of WHSUserPermission objects. The WHSUserPermission objects are what we played with in the last part of the tutorial.

At the top of our MainTabUserControl form, add a couple of named List<SharePermissions> objects. These will hold our known-good state and the current state of shared folder permissions.

List<SharePermissions> AcceptedPermissions;
List<SharePermissions> CurrentPermissions;

 115addlist

 

Step 2: Get ready for XML serialization

We need a way to save our new objects to disk. We'll do that with XML Serialization. Add the following using declarations to the top of the MainTabUserControl file:

using System.IO;
using System.Xml;
using System.Xml.Serialization;

 116addlist

We'll make use of an ApplicationFolder, a great Windows Home Server feature, to determine a physical location to save our XML file. Add a named instance of IApplicationFolder under our two List<SharePermissions> objects.

IApplicationFolder AppFolder;

 117addlist

The setup of an ApplicationFolder requires us to provide Windows Home Server with a unique identifier. Generate a random GUID from http://www.famkruithof.net/uuid/uuidgen, and use it in the code below (don't just reuse the GUID in the code below; you don't want to use the same application folder as another Add-In author).

We'll try to find our ApplicationFolder using this GUID, and if it doesn't exist (if this is the first time loading the Add-In in the console, for example) we'll ask WHS to create a new one.

Add the following code to the constructor for MainTabUserControl:

try
{
    this.AppFolder = whsInfoClass.GetApplicationFolder(new Guid("a607657c-6b11-45e5-8ba6-885c57b19b21"));
}
catch (Exception)
{
    this.AppFolder = whsInfoClass.CreateApplicationFolder(new Guid("a607657c-6b11-45e5-8ba6-885c57b19b21"), "My First Add-In");
}

 118addlist

 

Step 3: Populate and save our new object

We're going to move a few chunks of our previous logic around to make things a bit more usable. Comment out the bulk of our DisplaySharePermissions method like this:

private void DisplaySharePermissions()
{
    this.fancyListView1.Items.Clear();
 
    //foreach (IShareInfo2 info in whsInfoClass.GetShareInfo2())
    //{
    //    Array perms;
    //    info.GetPermissions(out perms);
    //    WHSUserPermission[] permissions = perms as WHSUserPermission[];
 
    //    for (int i = 0; i < permissions.Length; i++)
    //    {
    //        ListViewItem permissionItem = new ListViewItem(info.Name);
    //        permissionItem.SubItems.Add(permissions[i].userName);
    //        permissionItem.SubItems.Add(permissions[i].permission.ToString());
    //        fancyListView1.Items.Add(permissionItem);
    //    }
    //}
}

 

Under DisplaySharePermissions, add a new method called GetCurrentSharePermissions. This method does basically the same thing as the chunk of code we commented out above.

The method loops through the IShareInfo2 array returned by Windows Home Server, and creates new SharePermissions objects for each shared folder. It then adds each WHSUserPermission object to the List<WHSUserPermission> in the SharePermissions object.

Finally, we return the fully populated list that represents all the shared folders on our server and all the permissions of those shared folders.

private List<SharePermissions> GetCurrentSharePermissions()
{
    List<SharePermissions> currentPermissions = new List<SharePermissions>();
 
    foreach (IShareInfo2 info in whsInfoClass.GetShareInfo2())
    {
        Array perms;
        info.GetPermissions(out perms);
        WHSUserPermission[] permissions = perms as WHSUserPermission[];
 
        SharePermissions share = new SharePermissions();
        share.ShareName = info.Name;
 
        for (int i = 0; i < permissions.Length; i++)
        {
            share.Permissions.Add(permissions[i]);
        }
 
        currentPermissions.Add(share);
    }
 
    return currentPermissions;
}

 121addlist

We also need to load, or deserialize, our known-good state from disk when the Add-In opens. Add a new method under GetCurrentSharePermissions called GetAcceptedSharePermissions.

This method tries to retrieve our XML file from our ApplicationFolder and read it. If that fails, we assume this is the first time we've opened the Add-In (so we haven't saved a known-good state yet) and we just use GetCurrentPermissions to populate the object.

private List<SharePermissions> GetAcceptedSharePermissions()
{
    List<SharePermissions> acceptedPermissions;
 
    try
    {
        Stream XmlFileStream = new FileStream(AppFolder.Path + @"\" + "AcceptedPermissions.xml", FileMode.Open);
        XmlSerializer Serializer = new XmlSerializer(typeof(List<SharePermissions>));
 
        acceptedPermissions = (List<SharePermissions>)Serializer.Deserialize(XmlFileStream);
 
        XmlFileStream.Close();
    }
    catch (Exception)
    {
        acceptedPermissions = GetCurrentSharePermissions();
    }
 
    return acceptedPermissions;
}

 122addlist

We need to save, or serialize, our AcceptedPermissions object to disk so we can load it back next time the console opens. Add a new method called SaveAcceptedSharePermissions.

private void SaveAcceptedSharePermissions()
{
    try
    {
        Stream XmlFileStream = new FileStream(AppFolder.Path + @"\" + "AcceptedPermissions.xml", FileMode.Create, FileAccess.ReadWrite);
        XmlSerializer Serializer = new XmlSerializer(typeof(List<SharePermissions>));
        XmlTextWriter Writer = new XmlTextWriter(XmlFileStream, Encoding.UTF8);
        Writer.Formatting = Formatting.Indented;
 
        if (this.AcceptedPermissions != null)
        {
            Serializer.Serialize(Writer, this.AcceptedPermissions);
        }
 
        XmlFileStream.Close();
    }
    catch (Exception)
    {
        MessageBox.Show("Unable to save Accepted Permissions!");
    }
}

 123addlist

In our MainTabUserControl constructor, let's call our Get methods to populate the two List<SharePermissions> objects when the Add-In loads so we can start comparing them.

this.AcceptedPermissions = GetAcceptedSharePermissions();
this.CurrentPermissions = GetCurrentSharePermissions();

 124addlist

We'll also change the function of our button.

Edit the Click event handler for the button so that we're updating our known-good state to match the current state, saving it to disk, and then updating the GUI.

private void consoleToolBarButton1_Click(object sender, EventArgs e)
{
    //this.listBox1.Items.Clear();
    AcceptedPermissions = CurrentPermissions;
    SaveAcceptedSharePermissions();
    DisplaySharePermissions();
}

 128addlist

We don't really care about the other notifications we've been watching. They were a great way to demonstrate some WHS functionality, but we don't want to process them anymore.

Expand the INotificationCallback region if  it's closed.

  126addlist 127addlist

Then comment out our processing in the NotificationChanged method.

public void NotificationChanged(string UniqueID, WHS_Notification_Type Type, WHS_Notification_Severity Severity, int IsSuppressed, string textHeader, string textDescription, string helpFilename, string helpSection, string helpLinkText)
{
    //throw new NotImplementedException();
    //this.listBox1.Items.Add(Type + " " + Severity + ": " + textHeader + " " + textDescription);
}

 

Step 4: Comparing known-good and current states

Now we're going to do something with our two lists of shared folder permissions.

As administrators, we care if the permissions assigned to shared folders change. We want to compare the current state with our known-good state to see if anyone's been tampering with things they shouldn't.

We'll add a big chunk of code to our DisplaySharePermissions method. This method holds most of our logic.

First, we clear fancyListView1 to avoid constantly adding duplicate items.

Then we loop through our CurrentPermissions and compare them with our AcceptedPermissions. If a user has a different permission than what we expect, we flag that entry and mark it red. If this is a new user, or a new share, we mark that entry green.

You'll notice that we're calling BeginUpdate and EndUpdate on fancyListView1. This freezes the control until we're done updating it. If you don't do this, you'll get some nasty flickering or a completely scrambled view, depending on how you're updating the FancyListView.

private void DisplaySharePermissions()
{
    this.fancyListView1.BeginUpdate();
 
    this.fancyListView1.Items.Clear();
    this.fancyListView1.Groups.Clear();
 
    foreach (SharePermissions share in CurrentPermissions)
    {
        ListViewGroup group = new ListViewGroup(share.ShareName);
        fancyListView1.Groups.Add(group);
 
        foreach (WHSUserPermission permission in share.Permissions)
        {
            ListViewItem permissionItem = new ListViewItem(string.Empty);
            permissionItem.Group = group;
 
            permissionItem.SubItems.Add(permission.userName);
            permissionItem.SubItems.Add(permission.permission.ToString());
 
            foreach (SharePermissions acceptedShare in AcceptedPermissions)
            {
                if (acceptedShare.ShareName == share.ShareName)
                {
                    foreach (WHSUserPermission acceptedPermission in acceptedShare.Permissions)
                    {
                        if (acceptedPermission.userName == permission.userName)
                        {
                            if (acceptedPermission.permission != permission.permission)
                            {
                                permissionItem.ForeColor = Color.Red;
                            }
                            else
                            {
                                permissionItem.ForeColor = Color.Black;
                            }
 
                            break;
                        }
 
                        permissionItem.ForeColor = Color.Green;
                    }
 
                    break;
                }
                else
                {
                    permissionItem.ForeColor = Color.Black;
                }
 
                permissionItem.ForeColor = Color.Green;
            }
 
            fancyListView1.Items.Add(permissionItem);
        }
    }
 
    this.fancyListView1.EndUpdate();
}

 129addlist

 

Step 5: Tweaking the GUI

Go back to our MainTabUserControl design view by opening Solution Explorer and double-clicking MainTabUserControl.cs.

Remove listBox1 by selecting it once (Visual Studio has a tendency to freeze when you first open the designer; just let it finish, it hasn't locked up) and then use the delete key.

Drop a PictureBox in from the ToolBox, and set it to dock in parent (click the little arrow at the top of the PictureBox and select Dock in Parent).

 130addlist 131-add-list

Select our button and open the Properties pane. Change the Text property to "Accept Current Permissions".

 132-add-list

Right-click one of the panes of our SplitContainer and choose Select 'splitContainer1'. Open the Properties pane and set IsSplitterFixed to true, SpiltterWidth to 1, and SplitterDistance to 100.

  133addlist 134addlist

Save the updates we've made, and change back to code view (open Solution Explorer, right-click MainTabUserControl.cs and choose View Code).

Go crazy with some image manipulation to make our Add-In a little prettier. Add the following to our MainTabUserControl constructor.

string title = "My First Add-In";
StringFormat format = new StringFormat(StringFormatFlags.DirectionVertical);
Font font = new Font("Tahoma", 40, FontStyle.Bold);
 
Image image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics graphics = Graphics.FromImage(image);
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
 
graphics.FillRectangle(new SolidBrush(Color.FromArgb(228, 235, 243)), new Rectangle(0, 0, image.Width, image.Height));
graphics.DrawImage(Microsoft.HomeServer.Common.Client.CommonImages.WizardTopBannerImage, new Point(0,0));
graphics.DrawString(title, font, new SolidBrush(Color.FromArgb(57, 69, 90)), new PointF(15, 75), format);
 
this.pictureBox1.Image = image;

 135addlist

Time to build and deploy this magnificent thing. If you don't remember from last time:

Right-click the project in Solution Explorer and choose Build. Copy the compiled Add-In (probably in C:\Documents and Settings\Administrator\My Documents\Visual Studio 2008\Projects\My First Add-In\My First Add-In\bin\Release\, named HomeServerConsoleTab.My_First_Add_In.dll) to your Windows Home Server's C:\Program Files\Windows Home Server\ folder.

Open the console and enjoy the spectacle. Make sure to click the button to save our first known-good state.

 136addlist

Congratulations! You have an Add-In that actually does something interesting!

 

Step 6: Investigating functionality

Change some shared folder permissions and watch how the Add-In reacts. Click the button to accept these changes as our known good state.

  137addlist 138addlist

Play around with changing permissions, then close and reopen the console. Our previous known-good state is retrieved from disk and the permissions changes you made will still be marked red.

Have a look at D:\folders\{A607657C-6B11-45E5-8BA6-885C57B19B21}\ (or whatever your custom GUID is). You'll see our AcceptedPermissions.xml file that we saved to our ApplicationFolder. Open the file up to see the structure.

Obviously there are some design issues here.

What happens if someone changes permissions and then just clicks the "Accept Current Permissions" button? Well, we won't know that anything's wrong, will we? The Add-In will be doing what its told and saving the new known-good state.

We could get around that by saving each permission change to a log file somewhere, or by firing off a Windows Home Server health alert. But this is only a tutorial to get you started, and not a design guide for securely auditing shared folder permissions, right?

Next time, we'll cover deploying Windows Home Server Add-Ins properly, using MSI packages. After all, no user wants to manually copy a DLL file to their server to install an Add-In.

posted on Sunday, December 14, 2008 10:19 PM | Filed Under [ Windows Home Server Development ]

Comments

Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (JammerX19 @ 12/20/2008 12:29 PM)

After this step, I was able to build cleanly but the console won't load my add-in. Where can I find error message about this? There aren't any in the event viewer.

P.S. The line graphics.SmoothingMode = SmoothingMode.HighQuality had to be changed to graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality
 
Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (Sam Wood @ 12/20/2008 12:47 PM)

Hi Jammer,

Thanks for the feedback.

I'll be doing a tutorial on debugging soon, but you can find most console errors in C:\Documents and Settings\All Users\Application Data\Microsoft\Windows Home Server\Logs\HomeServerConsole.xxx.log (where xxx is a datestamp), on your server.

Is the console not loading the tab, or does the console crash when you try to access the tab?
 
Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (JammerX19 @ 12/21/2008 2:40 AM)

Here's what I'm finding:

[1]081220.083728.0156: Init: Error: Exception while instantiating HomeServerTabExtender in HomeServerConsoleTab.My_First_Add_In.dll: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.HomeServer.HomeServerConsoleTab.My_First_Add_In.MainTabUserControl.DisplaySharePermissions()
at Microsoft.HomeServer.HomeServerConsoleTab.My_First_Add_In.MainTabUserControl..ctor(Int32 width, Int32 height, IConsoleServices consoleServices)
at Microsoft.HomeServer.HomeServerConsoleTab.My_First_Add_In.HomeServerTabExtender..ctor(Int32 width, Int32 height, IConsoleServices consoleServices)
--- End of inner exception stack trace ---
at System.RuntimeMethodHandle._InvokeConstructor(Object[] args, SignatureStruct& signature, IntPtr declaringType)
at System.RuntimeMethodHandle.InvokeConstructor(Object[] args, SignatureStruct signature, RuntimeTypeHandle declaringType)
at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
at Microsoft.HomeServer.Controls.TabFinder.GetExtenderByType(Type theType, Assembly a, String name, Int32 width, Int32 height, IConsoleServices consoleServices)
at Microsoft.HomeServer.Controls.TabFinder.GetExtender(Assembly a, String typeName, String prefixedDllName, Int32 width, Int32 height, IConsoleServices consoleServices)
at Microsoft.HomeServer.Controls.TabFinder.FindTabs(Int32 tabWidth, Int32 tabHeight, Int32 settingsWidth, Int32 settingsHeight)

I replaced my entire DisplaySharePermissions routine with yours and the problem persists.
 
Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (JammerX19 @ 12/21/2008 2:47 AM)

I found my problem. I was calling DisplaySharePermissions too early in the constructor.

Great tutorial so far!
 
Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (Sam Wood @ 12/21/2008 8:40 AM)

Thanks Jammer, glad you're enjoying it.
 
Gravatar
# re: "Hello World" Windows Home Server Add-In: Part 4 (noor @ 5/13/2010 3:59 AM)

it is best soft

Post Comment

Title *
Name *
Email
Url
Comment *  
Remember me
Please add 1 and 5 and type the answer here:

Search

Site Sections

Recent Posts

Archives

Post Categories

WHS Add-In Tutorial

WHS Blogs

WHS Development