Monday, February 23, 2009

Bootstrapper: scrap the bootstrapper

Although the "bootstrapper" solution worked fine, the decision was made to scrap it completely and do things in a different way.

There are a few valid reasons for that:
- The application comes together with a few other device driver packages
- Currently, these packages are bundled into the .msi package and installed on the client computer, and from there they have to be installed manually
- This brings the current .msi package size to over 100MB in size
- Not every client computer would use all of the device drivers
- If the application will need to support other hardware in the future, using the same approach will keep the .msi package bloating more and more.

So, the task was changed. The requirement now is to have an application in a separate package, and all the device drivers in separate packages, but the installation process will let users select which device drivers they want to install.

After some investigation I came to the conclusion that this can not be done using the Visual Studio Setup and Deployment project. The main limiting issue here is the fact that an .msi installer can not be started from withing another .msi installer. Therefore, I can not launch my application installer, show the dialog with options, and proceed to installing these optional drivers.

A simple solution I came up with was to write a small 'wrapper' Windows Forms application. The application would present the user with multiple checkboxes - one for each optional component.

After the user makes the choices and presses the 'Install' button, the application would first read the xml file which lists all available components






...


The name/path pairs would be added to the

Dictionary packages;

DriverPackage1, ..., MainAppPackage will be set up as tags for the checkboxes at design time to simplify the functionality.

The application will then loop through all checkboxes and, if the checkbox is checked, will add the setup file path to the list.


string startupPath = Application.StartupPath;

string path;
List components = new List();

foreach (Control control in this.Controls)
{
CheckBox checkBox = control as CheckBox;

if (checkBox != null && checkBox.Checked)
{
if (packages.TryGetValue(checkBox.Tag.ToString(), out path))
{
components.Add(path);
}
}
}

Finally, the application will loop through the list of setup files, executing the installation process for each file and waiting for it to finish before launching next one.


foreach (string componentPath in components)
{
InstallComponent(startupPath + componentPath);
}

// ..........

private void InstallComponent(string filePath)
{
System.Diagnostics.Process installerProcess;

installerProcess = System.Diagnostics.Process.Start(filePath);

while (installerProcess.HasExited == false)
{
//indicate progress to user
Application.DoEvents();
System.Threading.Thread.Sleep(250);
}
}

Other small details include a progress bar, success messages etc., but the main idea should be clear.

by . Also posted on my website

Sunday, February 22, 2009

Bootstrapper adventures

I'm now at the stage of creating a setup package for the application that is going to use that magnificent 3M scanner. It has a setup and deployment project already, so I just rebuild it and try installing on the test desktop. However, during the setup process I end up with an error message.

After some research, I find out that there is some driver package that needs to be installed before my application install. If I install it manually first, the installation runs smoothly. So I have to include it in the installation somehow. How hard can that be? I have no experience with installation packages, but I get a hint that a custom action can help.

I do some research and soon enough I find out that

Custom Actions Management in Deployment

Actions can only be run at the end of an installation.

That is not what I need, the drivers absolutely have to be installed prior to the application installation. I research more and find a couple of links, which teach me how to use Orca and how to execute my Custom Actions whenever I like.

MSI Custom Action DLLCustom Action Run EXE

Great! Problem solved. I edit my place my Custom Action before the installation process starts. This time, however, I encounter a '2731' error. I'm not the first one to ever get this error, of course.

Problem when trying to install .NET framwork 2.0 during MSI install

"It is probably failing because you are trying to invoke an installer when an installer is already running. You need to install separate installers sequentially, not from within one another. You would need a bootstrapper to do that. "

Well, that's what I should have known in the very beginning. OK then, now to create a bootstrapper. (And what is the bootstrapper, by the way?)

Use the Visual Studio 2005 Bootstrapper to Kick-Start Your InstallationCreating a bootstrapper for a VS Shell application

These 2 pages give me some ideas. I locate my C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages folder, create product.xml and package.xml to be as simple as possible, and now I can choose my package from MySetupProject->Properties->Prerequisites.

The application now can be installed smoothly, but there is still one thing I am not happy about. The package consists of setup.exe, the .msi package, and a subfolder with my driver package. I do not want the subfolder, that might be confusing for the user or the subfolder may get 'lost' somewhere in the process of application distribution. I'm looking for the soluton:

Bootstrapper: How to compile the application and prerequisite in single .msi package?
IExpress Installer

And the IExpress seems to work fine for me. I create the single-file installation package, copy it to the test desktop and run ... just to be presented with another error. After examining the installation log, I realise that the IExpress did not extract my driver to the subfolder, but the installer expected to find it in the subfolder. Apparently, IExpress does not support the subfolders. I need another trick. A google search returns me to the page I have seen already and I read it again, carefully ... to the end.

Creating a bootstrapper for a VS Shell application

There it is, my solution:

Unfortunately, the MSBuild task doesn't provide the option to have the configuration resource use prerequisite installers found in the target directory, so you must manually update the appropriate resource file to remove the hard-coded path that looks for prerequisites in a sub-directory of the same name.

- Open the Setup.exe program in Visual Studio's resource editor
- Double-click the resource named, SETUPCFG in the 41 folder
- Search for the "Vs Shell\" string and delete the two occurrences that appear
- Save the resource file and the Setup.exe executable will be updated automatically
- Run iexpress
- Create a new package by following the IExpress wizard's steps and make sure to include the following files ...

Some careful setup.exe editing follows (first attempt was unsuccessful, I spoiled the .exe and had to rebuild my project again) and I have the complete solution - my single-file installation package, that has a prerequisite that is installed before the installation of the main application.

However, that was not the end ...

by . Also posted on my website

Wednesday, February 18, 2009

Mysterious validation function.

Making changes to some application and having some problems with validation, I came across this validation function:

public new bool Validate(bool someParameter)
{
bool blnResult = true;

if (name != null)
{
if (!name.Validate())
blnResult = false;
}

if (address != null)
{
if (!address.Validate(someParameter))
blnResult = false;
}

if (somethingElse != null)
{
if (!somethingElse.Validate())
blnResult = false;
}

if (someMore != null)
{
if (!someMore.Validate())
blnResult = false;
}

return blnResult;
}

So I asked myself, why would this function go through all validations each time even if it knows after the very first one that the blnResult is false and that will not change?

After some thought and investigation, the most likely answer is that the application was growing little by little. So, whenever its functionality was extended, say, from just keeping names to keeping names and addresses, the person currently in charge of the application would just copy and paste this bit

if (name != null)
{
if (!name.Validate())
return false;
}

replace name with address and move on.

After I made some small change, the function does not look much different, but I have much less troubles with validation now.

public new bool Validate(bool someParameter)
{
if (name != null)
{
if (!name.Validate())
return false;
}

if (address != null)
{
if (!address.Validate(someParameter))
return false;
}

if (somethingElse != null)
{
if (!somethingElse.Validate())
return false;
}

if (someMore != null)
{
if (!someMore.Validate())
return false;
}

return true;
}
by . Also posted on my website

Monday, February 16, 2009

A small functionality change.

Here's what can happen when a small functionality change is required in a fairly complex application. In this case, there are a few TabPages that are generated dynamically. Some of the TabPages have a 'Save Details' button.

The change: one of the TabPages now should NOT have a 'Save Details' button if the object is a 'new' object (not yet saved in the database). Easy!

Solution:

this.tabControl.Controls.Add(this.mySomethingTabPage);

HideOrShowSaveButton(this.mySomethingTabPage); // solution

...

public void HideOrShowSaveButton(TabPage tabPage)
{
//'save details' should not appear on the 'MySomething' tab page if
//the object is a new object

//find 'save details' button

ToolStripButton mySmallSaveButton = null;

foreach (Control c in this.mySomethingTabPage.Controls)
{
SomeDetails.SomeDetailsHeadingView pdhv = c as SomeDetails.SomeDetailsHeadingView;

if (pdhv != null)
{
foreach (Control c1 in pdhv.Controls)
{
ToolStrip headingToolStrip = c1 as ToolStrip;

if (headingToolStrip != null)
{
foreach (ToolStripItem item in headingToolStrip.Items)
{
if (item.Name == "SaveButton")
{
mySmallSaveButton = item as ToolStripButton;

//hide button on the 'someDetails' tab if new object
if (mySmallSaveButton != null)
{
if (Global.currentObject.IsNewObject)
{
mySmallSaveButton.Visible = false;
mySmallSaveButton.Enabled = false;
}
else
{
mySmallSaveButton.Visible = true;
mySmallSaveButton.Enabled = true;
}
return;
}
}
}
}
}
}
}
}

Had to hardcode the 'SaveButton' name which I would really want to avoid, but did not want to spend more time on that.

Thoughts: this application could have been designed a little better to make future changes not so painful.

by . Also posted on my website

Thursday, February 12, 2009

Asynchronous calls to a WebService

Figured out how to call a WebService asynchronously using a Callback in .NET. The process is fairly easy and straightforward.

This article Professional ASP.NET Web Services : Asynchronous Programming provided me with most of the information that I needed, and even had a few solutions discussed, of which I chose the one which suits me best

First of all, of course, I need a WebService

namespace AsyncService
{
///
/// Summary description for Service1
///

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX,
// uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class AsyncService : System.Web.Services.WebService
{

[WebMethod(Description="Returna a random value under 1000")]
public int[] GetRandomValue(int id, int delay)
{
Random random = new Random();
int randomValue = random.Next(1000);
int[] returnValue = new int[] { id, randomValue };
Thread.Sleep(delay);
return returnValue;
}
}
}

This service only returns a random number. It also returns the number after some delay to imitate that it is actually does something useful.

A Web Service proxy class provides a wrapper that lets me communicate to a WebService.

namespace AsyncCaller
{
[WebServiceBindingAttribute(Name = "AsyncRequestSoap", Namespace
= "http://tempuri.org/")]
public class AsyncCallerProxy : SoapHttpClientProtocol
{
public AsyncCallerProxy()
{
this.Url = "http://localhost/MyAsyncService/AsyncService.asmx";
}

[SoapDocumentMethodAttribute("http://tempuri.org/GetRandomValue",
Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
public int[] GetRandomValue(int id, int delay)
{
object[] results = this.Invoke("GetRandomValue", new object[]
{ id, delay });
return ((int[])results[0]);
}

public IAsyncResult BeginGetRandomValue(int id, int delay,
AsyncCallback callback, object asyncState)
{
return this.BeginInvoke("GetRandomValue", new object[] {
id, delay}, callback, asyncState);
}

public int[] EndGetRandomValue(IAsyncResult asyncResult)
{
object[] results = this.EndInvoke(asyncResult);
return ((int[])(results[0]));
}
}
}

Now I want to make a small demonstration of asynchronous communication to the WebService.

Before that I would need a very simple helper class to make it easy.

public class AsyncHelper
{
public AsyncHelper(int id)
{
this.HelperID = id;
Random random = new Random();
this.RandomDelay = random.Next(10000);
}

int _helperID = 0;

public int HelperID
{
get { return _helperID; }
set { _helperID = value; }
}

int _randomDelay = 0;

public int RandomDelay
{
get { return _randomDelay; }
set { _randomDelay = value; }
}

int _randomResult = 0;

public int RandomResult
{
get { return _randomResult; }
set { _randomResult = value; }
}
}

When an instatnce of the class is created, it is assigned a random delay value. I will pass it to the WebService and will get a response after a number of milliseconds defined by RandomDelay value.

On my demo application form I have a button and two DataGridViews.

When a button is pressed, an instance of the AsyncHelper class is created and added to the list of currently running requests, which is bound to the first DataGridView. The ID of the class instance and the delay value are passed to the WebService.

After the delay, the WebService returns the ID and the random ‘Result’. An instance of the AsyncHelper is found by ID, the result is assigned and the instance is removed from the current requests list and added to the processed requests list. If the button is pressed multiple times, a user can see multiple requests being added to the list and being returned by the WebService after the delays specified.

public partial class Form1 : Form
{
AsyncCallerProxy objWebService = new AsyncCallerProxy();

//counter for number of requests sent
private int _requestCounter = 0;
private List _asyncRequests = new List();
private List _asyncRequestsProcessed =
new List();

public Form1()
{
InitializeComponent();
bindingSourceRequests.DataSource = _asyncRequests;
dataGridViewRequests.DataSource = bindingSourceRequests;

bindingSourceRequestsProcessed.DataSource = _asyncRequestsProcessed;
dataGridViewRequestsProcessed.DataSource =
bindingSourceRequestsProcessed;

dataGridViewRequests.Columns[0].DataPropertyName = "HelperID";
dataGridViewRequests.Columns[1].DataPropertyName = "RandomDelay";

dataGridViewRequestsProcessed.Columns[0].DataPropertyName =
"HelperID";
dataGridViewRequestsProcessed.Columns[1].DataPropertyName =
"RandomDelay";
dataGridViewRequestsProcessed.Columns[2].DataPropertyName =
"RandomResult";
}

private void buttonRequest_Click(object sender, EventArgs e)
{
//create a new request and add it to request queue
AsyncHelper newRequest = new AsyncHelper(_requestCounter);
_requestCounter++;

_asyncRequests.Add(newRequest);

AsyncCallback asyncCallback = new AsyncCallback(MyCallBack);

IAsyncResult asyncResult;

asyncResult = objWebService.BeginGetRandomValue
(newRequest.HelperID, newRequest.RandomDelay, asyncCallback, null);

UpdateRequestQueueDisplay();
}

private void MyCallBack(IAsyncResult asyncResult)
{
int[] returnValue = objWebService.EndGetRandomValue(asyncResult);
int id = returnValue[0];
int result = returnValue[1];

AsyncHelper currentRequest =
_asyncRequests.Find(delegate(AsyncHelper testRequest)
{return testRequest.HelperID == id;});

//request is processed, remove it from the queue and add to processed
//requests list
if (currentRequest != null)
{
currentRequest.RandomResult = result;
_asyncRequestsProcessed.Add(currentRequest);
_asyncRequests.Remove(currentRequest);

UpdateRequestQueueDisplay();
}
}

private void UpdateRequestQueueDisplay()
{
//fixes the cross-thread issue while accessing the form controls
this.BeginInvoke(new MethodInvoker(delegate()
{
this.bindingSourceRequests.ResetBindings(false);
this.bindingSourceRequestsProcessed.ResetBindings(false);
}));

}
}
by . Also posted on my website

Wednesday, February 11, 2009

Top-level exception handling

Found a good article on exception handling. Top-level Exception Handling in Windows Forms Applications and followed some advice from it.

What it meant for me, basically, is that I changed the Program.cs file of my application from this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace MyNameSpace
{
static class Program
{
///
/// The main entry point for the application.
///

[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormUpdater());
}
}
}

to this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Threading;

namespace MyNameSpace
{
static class Program
{
///
/// The main entry point for the application.
///

[STAThread]
static void Main()
{
Application.ThreadException +=
new ThreadExceptionEventHandler(new
ThreadExceptionHandler().ApplicationThreadException);

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormUpdater());
}

public class ThreadExceptionHandler
{
public void ApplicationThreadException
(object sender, ThreadExceptionEventArgs e)
{
MessageBox.Show(e.Exception.Message, "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}

and that allowed me to get rid of a couple of dozens try/catch blocks in the application code without losing any exception handling functionality. Quite handy.

by . Also posted on my website

Tuesday, February 10, 2009

Scanner Update

I tried the solution suggested in this thread. HOW TO RETRIEVE INSTALLED SCANNER

I did the following:

  • Downloaded wiaaut.dll

  • Copied it to system32

  • Registered it with "regsvr32 wiaaut.dll" (successfully)

  • Added a reference to wiaaut.dll to my project in Visual Studio.NET

  • Checked that the Windows Image Acquisition (WIA) service is running
  • Next, I added and debugged the following code:

    WIA.DeviceManager manager = new WIA.DeviceManagerClass();
    WIA.DeviceManagerClass managerClass = new WIA.DeviceManagerClass();

    string wdeviceName = "";
    foreach (WIA.DeviceInfo info in manager.DeviceInfos)
    {
    if (info.Type == WIA.WiaDeviceType.ScannerDeviceType)
    {
    foreach (WIA.Property p in info.Properties)
    {
    if (p.Name == "Name")
    {
    wdeviceName = ((WIA.IProperty)p).get_Value().ToString();
    Console.WriteLine(wdeviceName);
    }
    }
    }
    }

    However, the manager.DeviceInfos is always empty. I have 2 scanners attached, one of them shows in Control Panel->Scanners and Cameras, one doesn't, and both show under "Imaging Devices" in Device manager.

    At this point, the only idea I have is that the scanner drivers just do not support WIA. As long as I have no way to check if this is true or not, I'll have to stick to the yesterday's solution.

    by . Also posted on my website

    Monday, February 9, 2009

    Finding a scanner

    I need to find out what scanners are attached to the computer. I also need to give the user of my application an option to select the default scanner, and to change this selection at any time. Fortunately, the scanner can only be one of a few models. Therefore, this is the solution I came up with so far:

    ArrayList scanners = new ArrayList();

    ManagementObjectSearcher search = new System.Management.ManagementObjectSearcher
    ("SELECT * From Win32_PnPEntity");

    ManagementObjectCollection deviceCollection = search.Get();

    foreach (ManagementObject info in deviceCollection)
    {
    string deviceName = Convert.ToString(info["Caption"]);

    if (/* check deviceName for certain substrings */)
    {
    scanners.Add(deviceName);
    }
    }

    However, there are at least two things that can be improved, though I don't know if they're possible.

    First, I would like to get only those devices that are under "Imaging devices" in Device Manager. That would be a huge improvement as I currently have almost 200 entries in the deviceCollection, and only 2 of them are under "Imaging Devices".

    Also, I would like to find a way to check if the device is a scanner. That would help to provide a "general" solution where the scanner attached may be of any model.

    by . Also posted on my website

    Thursday, February 5, 2009

    Wrote a Windows Service

    Wrote my first windows service today. Was not hard at all, I mostly followed the guide

    Simple Windows Service Sample

    However, the way the service in the sample is logging events was not suitable for me. I needed to log the events into the Application log of the Event Viewer. I also needed to catch exceptions and log them as errors in the Application log. So I used the powers of Enterprise Library event logging and exception handling. Firstly, I added references to EnterpriseLibrary and the corresponding 'usings' to the windows service


    using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
    using Microsoft.Practices.EnterpriseLibrary.Logging;

    Then, I added the loggingConfiguration and exceptionHandling sections to the appConfig file, that looked like this:


    <configSections>
    <section name="loggingConfiguration"
    type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings,
    Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=null" />
    <section name="exceptionHandling" type=
    "Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Configuration.
    ExceptionHandlingSettings,
    Microsoft.Practices.EnterpriseLibrary.ExceptionHandling,
    Version=2.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </configSections>

    ...

    <loggingConfiguration name="Logging Application Block" tracingEnabled="true"
    defaultCategory="" logWarningsWhenNoCategoriesMatch="true">
    <listeners>
    <add source=" " formatter="Text Formatter" log="Application"
    machineName="" listenerDataType=
    "Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.
    FormattedEventLogTraceListenerData,
    Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=null"
    traceOutputOptions="None" type="Microsoft.Practices.EnterpriseLibrary.Logging.
    TraceListeners.FormattedEventLogTraceListener,
    Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"
    name="MyService EventLog TraceListener" />
    </listeners>
    <formatters>
    <add template="Timestamp: {timestamp} Message: {message} Category: {category} "
    type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter,
    Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=null" name="Text Formatter" />
    </formatters>
    <categorySources>
    <add switchValue="All" name="MyService">
    <listeners>
    <add name="MyService EventLog TraceListener" />
    </listeners>
    </add>
    </categorySources>
    <specialSources>
    <allEvents switchValue="All" name="All Events" />
    <notProcessed switchValue="All" name="Unprocessed Category" />
    <errors switchValue="All" name="Logging Errors & Warnings">
    <listeners>
    <add name="MyService EventLog TraceListener" />
    </listeners>
    </errors>
    </specialSources>
    </loggingConfiguration>

    <exceptionHandling>
    <exceptionPolicies>
    <add name="PagingPolicy">
    <exceptionTypes>
    <add type="System.Exception, mscorlib, Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=b77a5c561934e089"
    postHandlingAction="None" name="Exception">
    <exceptionHandlers>
    <add logCategory="MyService" eventId="100" severity="Error"
    title="MyService Exception Handling" formatterType=
    "Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.TextExceptionFormatter,
    Microsoft.Practices.EnterpriseLibrary.ExceptionHandling, Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=null" priority="0" type=
    "Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging.LoggingExceptionHandler,
    Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging,
    Version=2.0.0.0, Culture=neutral,
    PublicKeyToken=null"
    name="MyService Logging Handler" />
    </exceptionHandlers>
    </add>
    </exceptionTypes>
    </add>
    </exceptionPolicies>
    </exceptionHandling>

    I'll be honest, I would not be able to explain every single line in this XML snippet. I just know that it works that way, and when I try removing some parts of it which seem to be unnecessary to me, the whole application usually starts failing.

    Next, I put the following into the OnStart method of the service:


    try
    {
    Logger.Write("MyService Process Started: " + getNow(), "MyService");
    timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
    timer.Interval = 10000;
    timer.Enabled = true;
    }
    catch (Exception ex)
    {
    ExceptionPolicy.HandleException(ex, "MyService");
    errorCount++;
    }

    and into the OnStop method


    Logger.Write("MyService Process Stopped: " + getNow(), "MyService");
    timer.Enabled = false;

    and into the timer_Elapsed method


    Logger.Write("running MyService process at " + getNow(), "MyService");
    RunMainFunction();

    (getNow() simply returns DateTime stamp in the required format)

    and I started getting events written into the Application log.

    Actually, logging events into the event log seems to be faster and easier than trying to debug the service. At least with my little service, where the whole cycle of stopping service -> uninstallation -> building a service + installation package -> installation -> starting a new version of a service can be done in under one minute.

    by . Also posted on my website

    Wednesday, February 4, 2009

    Team Foundation Server 2008 Adventures

    I’m playing with the Team Foundation Server 2008 these days. After I mastered the installation process, time has come to play around with security settings, group memberships and things like that.The first real issue I came across was having a problem with adding a Windows Group to TFS Licensed Users group. This is done through Team->Team Foundation Server Settings -> Group Memberships. The error message I got was very uninspiring and unhelpful.

    At the same time I could add single users from the same group without any problems. So my next step was to try to add every user from the group individually one by one. This way, after adding five users, I got the more helpful error message

    Aha! There is an error code this time. Now I can find out why. Turns out that the 5 user limitation is specific to the TFS – Workgroup edition. Ok, I do not even know for sure what is the edition I have installed and it does not say on the Help->About.

    This page comes very handy

    Which Version of Team Foundation Server Do I Have?

    So, I check the value of the registry key under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\TeamFoundation\Registration\Edition_CT And it is ‘Workgroup’ indeed. Confirmed!

    Now I need to upgrade to the Standard Edition. I have the key, but when I go to Add and Remove programs, run Change/Remove and select the option to Upgrade, the boxes where I’m supposed to enter the license key are grayed out! I’m stuck!

    Luckily, I find some help.

    Upgrading TFS 2008 Workgroup to Std. Edition

    Here is what the wise guys from Microsoft advise:

    1. Find a Setup.sdb file at the folder

    \Microsoft Visual Studio 2008 Team Foundation Server\Microsoft Visual Studio 2008 Team Foundation Server - ENU\

    2. Open the Setup.sdb file with text editor; you can back up the file before editing it.

    3. Remove the “[Product Key]” line and the PID line after it in the file.

    Try to upgrade again.

    And this works. I re-check the registry key value and, indeed, it has changed to “Full” which means Standard Edition. Hooray! However, when I try to add a 6th user to the TFS Licensed users, I still come up with the same error. Now I consider reinstalling from scratch! Next, I come across this discussion.

    Upgrading from limited version

    And the last bit of advice seems to be my case

    “If you are using the full RTM version (not the workgroup version) do not use the Licensed Users Group. It is not used by the full version but is still limited to 5 users. Just add the users to the project groups.”

    And it works too. I can add Windows Users and Groups to project groups.

    Finally, a few words about deleting projects.
    A simple way to delete the project forever is explained here

    Team System Delete Project

    “If you need to delete a Team System Project you need to do it through a command line utility that is installed with Team Explorer. There is no way to delete a project from Team System except through the command line tool, TFSDeleteProject.exe. This utility is in the c:\program files\Microsoft Visual Studio 8\Common7\IDE\ folder by default. To delete a project, use the following syntax:

    TFSDeleteProject /server:ServerName ProjectName”

    However, if you are getting the TF30063 error, it might be worth looking at this post, it worked like a charm in my situation:

    TF30063: You are not authorized to access...

    “Ever try to delete a team project and get the above message? Are you in the Team Foundation Administrators group and scratching your head?”

    by . Also posted on my website