Sunday, May 31, 2009

Programmatically change Web project settings from dynamic port to static

If you have a solution consisting of many Web applications that currently use dynamic ports for debugging and now you want to modify them to be static, it will be tedious and extremely time-consuming to go in each project and change it. I developed an application to resolve this. For the future sake, I create a custom project template so that I don't have to go in the project properties and change it whenever a new Web application is created. Indeed, with my own project template, I can customize other things like libraries, references and macros I always use in a project.

Currently the application supports VS.2008. It takes the solution file (.sln) as an input to locate the project locations, and then goes in each indiviual project file (e.g., .csproj and .vbproj) to alter the setting. It only targets Web application including WCF. but not Website because the Web project properties for a Website is stored in .sln file.

The idea behind this application is very simple.

Define the settings

First, I create a setting file called vs.xml.   Currently the port is default to 2332.   The text in light green background is similar to the text you may find in your Web project file.

<?xml version="1.0" encoding="utf-8" ?>
<VisualStudio target="Visual Studio 2008 ">
  <SolutionFile version="10.00" desc="Microsoft Visual Studio Solution File, Format Version">
    <Languages>
      <Langauge enabled="true">
        <Type>CSharp</Type>
        <GUID>{FAE04EC0-301F-11d3-BF4B-00C04F79EFBC}</GUID>
        <extension>.csproj</extension>
      </Langauge>
      <Lanaguage enabled="true">
        <Type>VisualBasic</Type>
        <GUID>{F184B08F-C81C-45f6-A57F-5ABD9991F28F}</GUID>
        <extension>.vbproj</extension>
      </Lanaguage>
    </Languages>
  </SolutionFile>
  <ProjectFile desc="Microsoft Visual Studio Project File">
    <Web> 

<!-- Begin Settings --> <ProjectExtensions> <VisualStudio> <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}"> <!-- Define Web Project Settings --> <WebProjectProperties> <UseIIS>False</UseIIS> <AutoAssignPort>False</AutoAssignPort> <DevelopmentServerPort>2332</DevelopmentServerPort> <DevelopmentServerVPath>/</DevelopmentServerVPath> <IISUrl> </IISUrl> <NTLMAuthentication>False</NTLMAuthentication> <UseCustomServer>False</UseCustomServer> <CustomServerUrl> </CustomServerUrl> <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile> </WebProjectProperties> <!-- End of Web Project Settings --> </FlavorProperties> </VisualStudio> </ProjectExtensions> <!-- End of Settings -->

</Web> </ProjectFile> </VisualStudio>

The above vs.xml can be altered to support VS.2005. But I am going to describe the details here.

Define data structure to contain the settings

Create a data structure to contain all the Web properties; they are:

KeyValue
UseIISFalse
AutoAssignPortFalse
DevelopmentServerPort2332
DevelopmentServerVPath/
IISUrl
NTLMAuthenticationFalse
UseCustomServerFalse
CustomServerUrl
SaveServerSettingsInUserFile

I use a Dictionary to capture this.

    private Dictionary<String, String> props;
When the application reads vs.xml, it will automatically load all the settings into this props Dictionary.

Function to alter the settings

The function SaveTo as shown below will update the settings into the project file based on the settings in props.   Note that my current version is slightly different now.

    /// <summary>
    /// Saves the settings to a specific file.  
    /// The project file must have ProjectExtensions/VisualStudio/FlavorProperties/WebProjectProperties
    /// nodes in order to be qualified for process.
    /// If the required elements under WebProjectProeperties node are missing from the specific document, 
    /// it will insert them.  
    /// The original elments in the document under that node remain intact if they don't match.
    /// </summary>
    /// <param name="fileName">the exact full filename of the project file for save</param>
    public void SaveTo(String fileName) {
      String namespaceUri = null, prefix = null;
      bool hasNamespace = this.HasNamespace(fileName, out namespaceUri, out prefix);

      XmlDocument xmlDoc = new XmlDocument();
      xmlDoc.Load(fileName);
 
      XPathNavigator navigator = xmlDoc.CreateNavigator();
      XmlNamespaceManager namespaceManager = new XmlNamespaceManager(navigator.NameTable);
      if (hasNamespace) {
        SetupNamespaceEnv(prefix);
        namespaceManager.AddNamespace(prefix, namespaceUri);
      }

      XPathNavigator result = null;

      if (navigator.HasChildren) 
        result = navigator.SelectSingleNode(GetTargetNodeXPathExpression(), namespaceManager);
      else 
        throw new XmlException("Invalid file format, or it is not a Web project file.");

      if (result == null)
        throw new XmlException("It is not a Web project file.");
      

// up to here, "result" should be at node "WebProjectProperties" Dictionary dup = this.CloneProperties(); if (result.MoveToFirstChild()) { do { result.SetValue(dup[result.Name]); dup.Remove(result.Name); } while (result.MoveToNext()); this.changed = false; // all properties updated. if (dup.Count > 0) { // Missing properties we need to save, do INSERT to file now...; result.MoveToParent(); // position back to "WebProjectProperties" node this.AppendChild(ref result, dup); } } else { // no child found at "WebProjectProperties" node this.AppendChild(ref result, dup); // add a child nod with all the needy settings } xmlDoc.Save(fileName); // Now is time to save to disk.

if (hasNamespace) { this.ReverseNamespaceEnv(); } }

The main purpose of this function does is to update the project settings:
  • First, it locates the <WebProjectProperties> node to begin the process.
      <ProjectExtensions>
        <VisualStudio>
          <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">            
            <WebProjectProperties>
            ...
            </WebProjectProperties>
          </FlavorProperties>
        </VisualStudio>
      </ProjectExtensions>
    
  • When the node is found, the function will temper the settings accordingly. All settings are stored inside the <WebProjectProperties> node. However,
    • If there is empty inside the <WebProjectProperties> node, all settings will be appended into it.
    • According to props, any missing setting(s) will be added accordingly.
    • Any extra settings cannot be found in props will remain intact.

Describe basic project file structure in Solution (.sln)

If you use a text editor to open .sln file and view it, you will find that each project may contain the following structure.

 Project("language_GUID") = "project_name", "project_location_or_file", "project_GUID"
   ProjectSection(...) = ...
   ...
   EndProjectSection
 EndProrject
A regular Windows or Web project will not contain ProjectSection. But if a Web project is type of Website, ProjectSection is used for storing WebsiteProperties because it doesn't own a project file. A WCF client may use WebsiteProperties for its ProjectDependencies. In sum, most projects only contain two lines: The first Project line and the EndProrject line. For example,
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTest", "Test\MyTest.csproj", "{7C0275FE-A91D-42E9-9EF1-5CCBC9D54C3C}"
EndProrject

Put everything together

All project locations registered with the solution are relative to the solution. Thus the location Test\MyTest.csproj in the above example is a relative path to its solution. As soon as the project location is found, convert it to the absolute path and then feed it to the function SaveTo, SaveTo will update the settings automatically.

In order to accomplish this, I create a Collection to contain all the project data found in the solution file.

    private Collection<ProjectIdentifier> projectCollection;
Each project data is defined in the following class:
  public class ProjectIdentifier {
    private String langGuid = null;
    private String name = null;
    private String location = null;
    private String guid = null;

    public ProjectIdentifier(String langGuid, String name, String location, String guid) {
      this.langGuid = langGuid;
      this.name = name;
      this.location = location;
      this.guid = guid;
    } 
    ...
  }

When everything is ready, loop through the projectCollection to update the project files:

  foreach (ProjectIdentifier project in c) {
    ...
    if (System.IO.File.Exists(project.Location)) {
      try {            
        ...
        if (project.IsWebProject()) {
         ...
            this.Backup(project.Location, this.txtTag.Text.Trim());

          propsBuilder.SaveTo(project.Location);
        ...
      }
      catch (Exception ex) {
        String err = String.Format("Error occurs: {0}{1}{2}",
                                   ex.Message, Environment.NewLine, ex.StackTrace);
        ...
      }
    }
    else { // file not found or is not a file.
      ...
    }
  } // foreach

Request my application

Before execution, my application will first back up the project file by default while it traverses the project folder. You can override this setting if you don't like it. The backup files can be programmatically removed in a later time when you feel comfortable with the changes. I don't know how useful for you but it certainly helps me to resolve a lot of tedious tasks.   Please feel free to leave me a message via the comment box for requesting this application if you think that it will be helpful to your issue.   Don't forget to leave your email for response. All application requests will NOT be left on comment.

Update August 6, 2009
On or off there are people requesting it. Here are the links where I put for storage so I am no longer to manage and distribute them myself. Please feel free to download it yourself. The code has been changed since this post.
Download: [ Source ] [ Binary ]

Note that: if you request it, then you use it as your own risk. I am not responsible for any damages that may occur.

Update July 15, 2011
Sorry, this application is no longer to download. It is hard for me to ensure that the files are still alive.