Thursday, April 30, 2009

Fix backcolor for tabcontrol in .net

Well, once again Microsoft did me in. First it was depreciating IVsaSite, GetGlobalIstance, and other scripting items. Then it was the BackColor Property for the TabControl.

The Problem
Now, Microsoft says right off for the BackColor property that "This member is not meaningful for this control." Why? Apparently they don't want developers monkeying with standard forms where the user should be able to change their Window's appearance through control panel.

Fair enough, but sometimes the app needs to look different. For instance, for our most recent update we have a background image; having the battleship gray across it doesn't look so pretty. The more annoying part is that in the IDE, the form looks perfect. I can't tell you how annoying it is that Microsoft knows what we want, because they put it right in the IDE, but when it's running, they say we can't have it. (Maybe they'll get it right for Windows 7.) ;)


ide normalThis screen shot taken in the IDE shows the form the way we want it.


locura configuration issueThis is the same form in run mode. Thanks, Microsoft! ;)

Now the easiest solution is to allow an override for BackColor and BackgroundImage in the TabControl, but that would require Microsoft to realize the error of their ways. Instead, we will have to code around their "behavior by design". ;)

The Research
Initially, I found this post on setting the DrawMode to OwnerDrawFixed (I wonder if that means they admit the other is broken ;) ) It requires you to attach to the DrawItem event. This did lead me down the right path, but it only changed the tabs and then only their color.

The next post I found had a comment by LauraM that showed me how to change the extended part of the tab strip: the strip to the right of the tabs. This got me closer, but what I really needed to do was get the image from the parent and bring it through.

At this point it was all up to me. What I needed was more complex than what it appeared everyone else needed: I needed to have the underlying image or background color show through, depending on which was on the parent control, and if there were images on the tabs I needed those to show through as well. All this was in addition to setting the background color of the tabs to begin with.

The Solution
I started with the code that these posts brought me and altered it to make a more versatile setup. For this setup, there are three initial steps:
  1. Change the DrawMode property to OwnerDrawFixed.

  2. Attach to the DrawItem event. With the versatility of the method we will create, you will be able to use it for each tab control on the form.

  3. Create the method signatures.

(At this point if you just want code the entire method is at the bottom.)

Note that as soon as you change the DrawMode, the perfect TabControl you had in the IDE now becomes completely blank. It appears you can either have it work right in the IDE or in run mode, but not both. It's the kind of thrilling craziness that makes you glad you signed up for this career. ;)

ide with directdraw
The Code
As we want this to work for all our TabControls, we want two signatures: one that allows us to pass in an associated ImageList as needed, and one that doesn't require one. The one with fewer parameters will simply call the other with a null value:

   /// <summary>Draws the specified tab control.</summary>
   /// <param name="tabControl">The control on which to draw.</param>
   /// <param name="e">The event arguments.</param>
   private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e)
   {
      DrawTabControlTabs(tabControl, e, null);
   }
   /// <summary>Draws the specified tab control.</summary>
   /// <param name="tabControl">The control on which to draw.</param>
   /// <param name="e">The event arguments.</param>
   /// <param name="images">The images for the tab control.</param>
   private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e, ImageList images)
   {
      ...
   }


Now each of the TabControls that use this must have the DrawItem event filled out. These events do nothing more than call DrawTabControlTabs, passing the TabControl to draw, and the DrawItemEventArgs parameter that was passed to the event. Those that have icons, need to pass a third parameter of an ImageList.

   /// <summary>Occurs when the items is drawn.</summary>
   /// <param name="sender">The object that fired the event.
   /// <param name="e">The arguments.</param>
   private void tabSections_DrawItem(object sender, DrawItemEventArgs e)
   {
      DrawTabControlTabs(tabSections, e, this.icons);
   }


Now, luckily, with everything that's going on it all fits in the one method. It's a bit longer than I typically like in a method (I love the nice short methods), but it's reasonable. The method consists of six basic steps.

Step 1: Get the bounding rectangle for the end of the tab strip

   // Get the bounding end of tab strip rectangles.
   Rectangle tabstripEndRect = tabControl.GetTabRect(tabControl.TabPages.Count - 1);
   RectangleF tabstripEndRectF = new RectangleF(tabstripEndRect.X + tabstripEndRect.Width, tabstripEndRect.Y - 5,
tabControl.Width - (tabstripEndRect.X + tabstripEndRect.Width), tabstripEndRect.Height + 5);


Step 2: Fill the end using the parent's background image or color
Note that for using the background image we need to get the source coordinates on the parent control or form so we can pull the part of the image precisely under the area we want to draw it.

   // First, do the end of the tab strip.
   // If we have an image use it.
   if (tabControl.Parent.BackgroundImage != null)
   {
      RectangleF src = new RectangleF(tabstripEndRectF.X + tabControl.Left, tabstripEndRectF.Y + tabControl.Top, tabstripEndRectF.Width, tabstripEndRectF.Height);
      e.Graphics.DrawImage(tabControl.Parent.BackgroundImage, tabstripEndRectF, src, GraphicsUnit.Pixel);
   }
   // If we have no image, use the background color.
   else
   {
      using (Brush backBrush = new SolidBrush(tabControl.Parent.BackColor))
      {
         e.Graphics.FillRectangle(backBrush, tabstripEndRectF);
      }
   }


Step 3: Pull the tab's color and name and apply the background color to the tab
Now it's worth noting at this point that the DrawItem event is fired for the drawing of each TabPage in the TabControl. Since this is a UI method (typically where the speed bottleneck is the user) it's not of great concern that in the previous steps we are drawing the parts of the TabControl that only need to be drawn once per control as many times as we have tabs. I did not see a speed issue with this and storing state may get a little nasty so I am leaving it. You may wish to do something differently. The code from this step on is for the tabs themselves.

   // Set up the page and the various pieces.
   TabPage page = tabControl.TabPages[e.Index];
   Brush BackBrush = new SolidBrush(page.BackColor);
   Brush ForeBrush = new SolidBrush(page.ForeColor);
   string TabName = page.Text;

   // Set up the offset for an icon, the bounding rectangle and image size and then fill the background.
   int iconOffset = 0;
   Rectangle tabBackgroundRect = e.Bounds;
   e.Graphics.FillRectangle(BackBrush, tabBackgroundRect);


Step 4: Put any icons that exist on the tabs
Now this works whether you used an index or a key, but make sure you pass an ImageList to the method or this code won't get called.

   // If we have images, process them.
   if (images != null)
   {
      // Get sice and image.
      Size size = images.ImageSize;
      Image icon = null;
      if (page.ImageIndex > -1)
         icon = images.Images[page.ImageIndex];
      else if (page.ImageKey != "")
         icon = images.Images[page.ImageKey];

      // If there is an image, use it.
      if (icon != null)
      {
         Point startPoint = new Point(tabBackgroundRect.X + 2 + ((tabBackgroundRect.Height - size.Height) / 2),
      tabBackgroundRect.Y + 2 + ((tabBackgroundRect.Height - size.Height) / 2));
      e.Graphics.DrawImage(icon, new Rectangle(startPoint, size));
      iconOffset = size.Width + 4;
      }
   }


Step 5: Spit out the text

   // Draw out the label.
   Rectangle labelRect = new Rectangle(tabBackgroundRect.X + iconOffset, tabBackgroundRect.Y + 3,
tabBackgroundRect.Width - iconOffset, tabBackgroundRect.Height - 3);
   StringFormat sf = new StringFormat();
   sf.Alignment = StringAlignment.Center;
   e.Graphics.DrawString(TabName, e.Font, ForeBrush, labelRect, sf);


Step 6: Clean-up

   //Dispose objects
   sf.Dispose();
   BackBrush.Dispose();
   ForeBrush.Dispose();


The Fixed Form
Now here is the fixed form with the background image pulled through (and on other tabs the background color). Also icons show where appropriate.

locura configuration utility fixed
The Method
Here is the full method. It's annoying as hell that we have to code this to make these things work, but there's not much to it. Happily this is fairly extensible supporting background image or backcolor bleed through and images by key or index. Hopefully if you were in the same boat I was, you will find this code helpful to the point you can quickly get on with the important parts of coding your project and not the workarounds.

private void DrawTabControlTabs(TabControl tabControl, DrawItemEventArgs e, ImageList images)
{
   // Get the bounding end of tab strip rectangles.
   Rectangle tabstripEndRect = tabControl.GetTabRect(tabControl.TabPages.Count - 1);
   RectangleF tabstripEndRectF = new RectangleF(tabstripEndRect.X + tabstripEndRect.Width, tabstripEndRect.Y - 5,
tabControl.Width - (tabstripEndRect.X + tabstripEndRect.Width), tabstripEndRect.Height + 5);

   // First, do the end of the tab strip.
   // If we have an image use it.
   if (tabControl.Parent.BackgroundImage != null)
   {
      RectangleF src = new RectangleF(tabstripEndRectF.X + tabControl.Left, tabstripEndRectF.Y + tabControl.Top, tabstripEndRectF.Width, tabstripEndRectF.Height);
      e.Graphics.DrawImage(tabControl.Parent.BackgroundImage, tabstripEndRectF, src, GraphicsUnit.Pixel);
   }
   // If we have no image, use the background color.
   else
   {
      using (Brush backBrush = new SolidBrush(tabControl.Parent.BackColor))
      {
         e.Graphics.FillRectangle(backBrush, tabstripEndRectF);
      }
   }

   // Set up the page and the various pieces.
   TabPage page = tabControl.TabPages[e.Index];
   Brush BackBrush = new SolidBrush(page.BackColor);
   Brush ForeBrush = new SolidBrush(page.ForeColor);
   string TabName = page.Text;

   // Set up the offset for an icon, the bounding rectangle and image size and then fill the background.
   int iconOffset = 0;
   Rectangle tabBackgroundRect = e.Bounds;
   e.Graphics.FillRectangle(BackBrush, tabBackgroundRect);

   // If we have images, process them.
   if (images != null)
   {
      // Get sice and image.
      Size size = images.ImageSize;
      Image icon = null;
      if (page.ImageIndex > -1)
         icon = images.Images[page.ImageIndex];
      else if (page.ImageKey != "")
         icon = images.Images[page.ImageKey];

      // If there is an image, use it.
      if (icon != null)
      {
         Point startPoint = new Point(tabBackgroundRect.X + 2 + ((tabBackgroundRect.Height - size.Height) / 2),
      tabBackgroundRect.Y + 2 + ((tabBackgroundRect.Height - size.Height) / 2));
      e.Graphics.DrawImage(icon, new Rectangle(startPoint, size));
      iconOffset = size.Width + 4;
      }
   }

   // Draw out the label.
   Rectangle labelRect = new Rectangle(tabBackgroundRect.X + iconOffset, tabBackgroundRect.Y + 3,
tabBackgroundRect.Width - iconOffset, tabBackgroundRect.Height - 3);
   StringFormat sf = new StringFormat();
   sf.Alignment = StringAlignment.Center;
   e.Graphics.DrawString(TabName, e.Font, ForeBrush, labelRect, sf);

   //Dispose objects
   sf.Dispose();
   BackBrush.Dispose();
   ForeBrush.Dispose();
}

Tuesday, April 28, 2009

Windows 7 - also late to the party

windows 7

It wasn't long ago iPhone 3.0 was announced including media streaming, to which we pointed out that the Locura Media server already has that (and we have plans to release an iPhone app to complement our media server). Now Windows 7 has announced it will also have these features, but who knows when it will come out.

However, with Windows 7 Windows Media Player is required on the server and the client. The Locura Media server doesn't have this restriction. It requires a web browser and a device that can handle media. That's why it works on the iPod Touch, iPhone, PSP, Mac, PC, and more.

Glad you could come to the party, Windows 7. It would be nice if you could have done it in a way that is cross-platform.

Wednesday, April 22, 2009

Ready for iphone development

mac mini

Okay, not quite ready, but one step away. We are still working on the release of our next version of the Locura Media Server, hard at work on adding new features in addition to getting around .NET conversion issues.

But the thing is we have an Mac now and are delving into the SDK to be ready for our next project to create a simple, easy to use iPhone app to work with the media server (we already beat Apple to the punch on streaming media). I personally am still getting used to the Mac: it's a little tricky sometimes, but overall I am having fun. In fact, this is my first post from my first Mac.

So, keep an eye out. The VillainousMind app store is not far away.

Wednesday, April 15, 2009

Converting VSA global items to CodeDomProvider global items

visual studio logo

Key Audience: Those needing .NET scripting access to code objects
This post is for all the people out there like me that need to keep scripts created for .NET 1.1 alive today using later versions of .NET, particularly those people who rely on GetGlobalInstance to create global items to attach to objects used in code so that they can be accessed by a script (Like the Response and Request objects are in ASP, or in our case, the Locura Media Server).

Keywords you may be looking for are: IVsaGlobalItem, <engine>.Items.CreateItem, and VsaItemType.AppGlobal.

Introduction
It wasn’t long ago I personally had the task of updating our personal media server to the latest version of .NET for our upcoming redesign.



Step one was simply pulling all the dll's into Visual Studio 2008 and hitting Shift-Ctrl-B (Build All). Now it is common coding policy at VillainousMind, Inc. to treat warnings as errors, keeping with Code Once philosophy. Unfortunately, that lead to the following compiler-created message:

Use of this type is not recommended because it is being deprecated in Visual Studio 2005; there will be no replacement for this feature. Please see the ICodeCompiler documentation for additional help.


Hitting this roadblock was annoying. Not only did Microsoft deprecate the only scripting facilities they built into our current .NET flavor at the time, but they left no easy tutorial on how to convert.

The first thing I did was search for how to use the ICodeCompiler correctly, which lead me to the CodeDomProvider. Typically the suggestion is to use this with predefined inherited classes:

codedomcompiler

As VBScript.NET is now hard to come by, I had to use CodeDomProvider.CreateProvider(<language>). Once I made it past this series of discoveries, I realized the best thing to do was to take the information I had found and make a test project. The final project can be downloaded freely from the VillainousMind website. You may simply want to use this code and snoop around, using this post simply for reference, or you may want to read the blog and use the code as supporting material. Either way, feel free to use it to work out your own solutions as needed.

The Problem
One of the key aspects of our media server is that we provide robust scripting capabilities for a wide variety of user-customization. In order to apply the customer service golden rule we have to do what is in our power to make existing scripts work with our new engine. Our current engine works much like classic ASP, or the actual ASPX pages in .NET where HTML and script can be combined. In fact, since the Locura Media Server is a web server, many of the same objects are present: Server, Response, Request, etc.

The problem comes in right here: these global scripting objects, objects that are actively used in regular code but must be available to scripts, don't have an easy method of exposure, or at least not any method that I could easily find through Google searches. So here was my dilemma: how do I keep existing scripts viable by providing global access to my code objects?

The Solution
When I finally realized the solution I was glad that we chose VBScript.NET as our default scripting language (of course with 1.1 we had little choice). With VBScript.NET modules were in play meaning I could add global variables. Now other languages may not afford this, but with a simple string.Replace() and static methods, you should be fairly close.

The Test Project Implementation

The Script
For my test project I first scripted a module:

   Module GlobalObjects

      Public Response As Response

   End Module


Next, I had to create an object who's only real job is to set the global object. (There may be a better way to do this, but I could not find a way of setting a global object directly; if you find it, please post a comment.) This object I called Middleman:

   Public Class MiddleMan

      Public Shared Sub SetResponse(globalResponse As Response)
         Response = globalResponse
      End Sub

   End Class


At this point all existing scripts that need a global Response object should work. For my test project I created a simple test script object to act as the "pre-existing" script. This is the script that actually uses the global Response object:

   Public Class Test

      Public Sub TestToFile()
         Response.WriteLine(String.Format("In script - initial value: {0}", Response.TestValue.ToString()))
         Response.TestValue = 2349
         Response.WriteLine(String.Format("In script - altered value: {0}", Response.TestValue.ToString()))
      End Sub

   End Class


Note that I write out Response.TestValue, change it, then write it out again. Later in this article, the code that compiles the script will do something similar so that we can verify that changes to the object in code affect the object in script and vice versa.

The Compilation
First of all, before any of this will work I had to create a separate project containing the object that I wanted available in script. In my sample this is ScriptingTestLibrary which contains the Response class.

For compilation, I want to include this dll in my referenced assemblies:

   // Set the standard compiler parameters.
   CompilerParameters paramList = new CompilerParameters();
   paramList.GenerateExecutable = false;
   paramList.GenerateInMemory = true;
   paramList.IncludeDebugInformation = false;

   // Set the referenced libraries.
   paramList.ReferencedAssemblies.Add("System.dll");
   paramList.ReferencedAssemblies.Add("System.Windows.Forms.dll");
   paramList.ReferencedAssemblies.Add("ScriptingTestLibrary.dll");

   // Get the scripts and compile.
   string[] scripts = new string[1] { GetScriptFromFile(filename) };
   CodeDomProvider provider = CodeDomProvider.CreateProvider(language);
   return provider.CompileAssemblyFromSource(paramList, scripts);


Obviously the Compile method I created is limited as it only allows for one script to be passed. Normally this would not be a scalable solution, but as this is only a proof-of-concept program it makes more sense to code for time instead of scalability.

At this point I use the returned CompilerResults object to log any error information or a success message. Then, if everything compiled correctly, I call the Run method which does the work of setting and calling my global objects.

Setting the Global Objects
The last step is setting the global objects in code. The first step is to create an instance of both the entry object, the object that starts the actual script, and the middleman object, the object that sets the global objects.


   // Pull the ext object out.
   object entryObject = compiledAssembly.CreateInstance(entryClassName);
   object middleManObject = compiledAssembly.CreateInstance(MiddlemanClassName);


Next we call the method on the middleman object that sets the global objects. Note that the globalItems collection is a list of GlobalItems structs which contain the object and the method name called on the middleman object. This is passed into the Run method.


   // Here is where we hook into our existing object.
   foreach (GlobalItem globalItem in globalItems)
      middleManObject.GetType().InvokeMember(globalItem.SetMethodName, BindingFlags.InvokeMethod, null, middleManObject, new object[1] { globalItem.Item });


Finally, we invoke the entry method on the entry object.


   // Run the entry object and method.
   entryObject.GetType().InvokeMember(entryClassMethod, BindingFlags.InvokeMethod, null, entryObject, null);


Testing the code
To make all this work I created a test UI where I can provide the key pieces of information to compile the program and run it. The key to this is in the CompileAndRun method in the ScriptFileCompilerForm object.

script file compiler sample app

The first thing is creating a Response object, setting it's test value, and creating a GlobalItem containing this object.

   // Set up the global response object and set the initial value.
   Response response = new Response(outputFilename);
   response.TestValue = 6320;

   // Set up the global items.
   GlobalItem globalItem = new GlobalItem();
   globalItem.Item = response;
   globalItem.SetMethodName = setMethodName;


The next step is the meat of this method: The initial value is captured, the script is compiled and run, and the initial value and altered value are output.


   // Save the test value, Compile and run it, then print out the altered test value.
   int initialTestValue = response.TestValue;
   ScriptingEngine engine = new ScriptingEngine(outputFilename);
   bool success = engine.CompileAndRun(language, filename, new GlobalItem[1] { globalItem }, entryClassName, entryClassMethod, MiddlemanClassName);
   if (success) response.WriteLine(string.Format("In code - initial value: {0}\r\nIn code - altered value: {1}", initialTestValue, response.TestValue));


The rest of the method does nothing more than show the outcome and ask the user is she wants to see the results.

   // Show results.
   string resultText = "succeeded";
   if (! success) resultText = "failed";
   if (MessageBox.Show(string.Format("The compilation and run {0}.\r\rDo you want view the output?", resultText),
      "Compilation Results", MessageBoxButtons.YesNo) == DialogResult.Yes)
      ShowOutput();


Finally, when the program is run and the script compiles and runs successfully, the following output is created showing that the changes to the object in script affected the object in code and vice versa. In other words, this object is one and the same, not a copy.

Build successful

In script - initial value: 6320
In script - altered value: 2349
In code - initial value: 6320
In code - altered value: 2349


There is a bit more to the program, but overall it's pretty straightforward both from a code standpoint and a UI standpoint. Again, the code is freely available from the VillainousMind website.

Summary
Overall, this is a simple solution to implement global objects in VBScript.NET in order to access code objects with script. Its purpose is to make scripts created with VSA compatible with newer .NET methods of scripting using ICodeCompiler (or CodeDomProvider).

This method creates a VBScript.NET module for global objects and a VBScript.NET middleman object to set this object. It also uses a referenced assembly in order to use this global object. The code then uses the middleman object to set the global object which can then be used in script.

Feel free to comment any thoughts or additions to this methodology. I would be happy to hear a similar method for C# using static methods and some type of alias instead of using string.replace, but as it is not needed for my project, it is on a back-burner list.

If you were running across the same issues I was, I hope this was helpful to you.