Archive for the ‘ SmartClient ’ Category

ClickOnce – File Already Exists

The Problem

If your deploy a new version of your app and your users see “Cannot Start Application” when updating:

There could be many reasons, this quick post provides a few places to look to find what may be causing the File Already Exists problem.

Click on ‘Details’…and if your error looks something like this…then this most may help:

ERROR SUMMARY
	Below is a summary of the errors, details of these errors are listed later in the log.
	* Activation of ...\Start Menu\Programs\MyApp\MyApp\MyApp.appref-ms| resulted in exception. Following failure messages were detected:
		+ The file '...\Local Settings\Temp\Deployment\GOYP6PP3.TRR\61YND8Y2.Q2A\NaughtyFile.dll' already exists.

Where to look

Open up your ..\Application Files\MyApp1.0.0.0 directory (where 1.0.0.0 is the current build number)

Find and open the .manifest file, should be called MyApp.manifest.

Search for NaughtFile.dll (in the error above, NaughtyFile.dll was the name of the file that already exists), if your problem is the same as mine then you should find two references to this file in the manifest. In my case I had one reference which was a dependentAssembly and another which was a file.

Now, the project you are publishing may not have a direct reference to a dll and yet it still appears in the list of dependentAssemblies because a project it references does directly reference the dll. The easiest way to check this list is to go to the project properties, Publish Tab and open Application Files…

From there you see the list of files that are included, but I’ve noticed sometimes the list is not 100% truthful, if weird stuff is happening, open your project file in Notepad and closely examine the list of included and excluded files.

Note that you can only exclude a file in this list if the project has a direct reference to the file (this may not be true, but I just tried it and it didn’t exclude and I don’t have time for further investigation).

So our trouble, our extra ‘file’ reference came from an extension to msBuild that we have to explicitly include certain files (needed if you need the dll but cannot add a reference for example), that looks a little like this….

  <ItemGroup>
    <AdditionalPublishFile Include="..\..\NaughtyFileLocation\*">
      <Visible>False</Visible>
    </AdditionalPublishFile>
....

By changing the explicit inclusion to ‘not’ include that specific file (we needed other files in that directory), our problem went away. The key is ensuring that your manifest only has one entry for each file that is included…the trick is figuring out why there are multiple entries!

Good luck!

file:///C:/Dev/Release/Master/PmsMasterBuild_2.0.1.11.7zERROR SUMMARY
	Below is a summary of the errors, details of these errors are listed later in the log.
	* Activation of C:\Documents and Settings\mark.wallis\Start Menu\Programs\PMS Smart Client\PMS Smart Client\PMS Smart Client.appref-ms| resulted in exception. Following failure messages were detected:
		+ The file 'C:\Documents and Settings\mark.wallis\Local Settings\Temp\Deployment\GOYP6PP3.TRR\61YND8Y2.Q2A\EcsLite.Downloader.dll' already exists.

ClickOnce Master Build

Previously I have posted about how to publish a ClickOnce release for multiple environments. Whilst this works well there are two reasons (that I can think of) why it is not appropriate or good enough.

First, you or your manager may be a bit of a purist in terms of releasing to Production ‘exactly’ what has been tested, specifically the exact same compiled assemblies.

Second, you may need to create a release for several different customers who each have several of their own ‘environments’ but you do not want to provide them with your source code.

Third…both of the above. 🙂

When you ‘Publish’ with MsBuild it requires the source code and it recompiles your assemblies. So how can we easily create releases for multiple environments but only Publish once? This posting offers one solution which has worked for me in the past, it’s not a complete ‘how to’ guide I just wanted to cover the idea and problems I ran into along the way.

Master Build Environment

Make sure you take a look at my previous posting as this process builds on those ideas.

One of the final steps of this process is to re-sign the setup.exe, however in order for that final step to work you have to change one of your ClickOnce deployment settings in your .csproj file.

<SignManifests>false</SignManifests>

SignManifests must be false! Otherwise the signtool gets confused when re-signing the setup.exe. Check this out for a bit more info on this known issue.

So the first thing to do is create a BuildEnvironment called Master. The configuration details of this (web service addresses or database connnection strings etc) should point to nowhere, i.e. if someone deployed this build then it should not work. Publishing this is easy just follow the steps defined in the multiple environment posting, the process of creating a release with environment specific information that is tricky.

MsBuild Changes for Master Build Publish

When you publish with ClickOnce it creates a folder ‘Application Files’ where it puts the contents of the release. The space in that file name causes a problem with MAGE when re-signing the app. Now the only way I could see to change this was to cut and paste the _CopyFilesToPublishFolder target from Microsoft.Common.Targets and modifying the _DeploymentApplicationFolderName property in my version to remove the space. I think it is a really BAD idea to change the Microsoft.Common.Targets directly so that option was ruled out immediately and as you can see in the code below, Application Files (version below already has the space removed) is a hardcoded value, not a settable property…therefore even though it is ugly, cut and paste seems the only viable solution.

      <!--
	  This Target code has been cut and paste from Microsoft.Common.Targets.
	  Only ONE thing has been changed.
	  Application Files changed to ApplicationFiles (i.e. the space removed)
	  The only other way of changing that value is to modify Microsoft.Common.Targets
	  which is a really bad idea (would have to ensure this was changed on ALL machines.)

	  The space causes a problem when re-signing the app with MAGE
    ============================================================
                                        _CopyFilesToPublishFolder
    ============================================================
    -->
    <Target
        Name="_CopyFilesToPublishFolder">

        <!-- Compute name of application folder, which includes the assembly name plus formatted application version.
             The application version is formatted to use "_" in place of "." chars (i.e. "1_0_0_0" instead of "1.0.0.0").
             This is done because some servers misinterpret "." as a file extension. -->
        <FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)" FormatType="Path">
            <Output TaskParameter="OutputVersion" PropertyName="_DeploymentApplicationVersionFragment"/>
        </FormatVersion>

        <PropertyGroup>
            <_DeploymentApplicationFolderName>ApplicationFiles\$(AssemblyName)_$(_DeploymentApplicationVersionFragment)</_DeploymentApplicationFolderName>
            <_DeploymentApplicationDir>$(PublishDir)$(_DeploymentApplicationFolderName)\</_DeploymentApplicationDir>
        </PropertyGroup>

        <!-- Copy files to publish folder -->
        <Copy
            SourceFiles=
                "@(_ApplicationManifestFinal);
                @(_DeploymentResolvedManifestEntryPoint);
                @(_DeploymentManifestFiles);
                @(ReferenceComWrappersToCopyLocal);
                @(ResolvedIsolatedComModules);
                @(_DeploymentLooseManifestFile)"
            DestinationFiles=
                "@(_ApplicationManifestFinal->'$(_DeploymentApplicationDir)%(TargetPath)');
                @(_DeploymentManifestEntryPoint->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)');
                @(_DeploymentManifestFiles->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)');
                @(ReferenceComWrappersToCopyLocal->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)');
                @(ResolvedIsolatedComModules->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)');
                @(_DeploymentLooseManifestFile->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"/>
        <Copy
            SourceFiles="@(_DeploymentManifestDependencies)"
            DestinationFiles="@(_DeploymentManifestDependencies->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
            Condition="'%(_DeploymentManifestDependencies.DependencyType)'=='Install'"/>
        <Copy
            SourceFiles="@(_ReferenceScatterPaths)"
            DestinationFiles="@(_ReferenceScatterPaths->'$(_DeploymentApplicationDir)%(Filename)%(Extension)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"/>
        <FormatUrl InputUrl="$(_DeploymentApplicationUrl)">
            <Output TaskParameter="OutputUrl" PropertyName="_DeploymentFormattedApplicationUrl"/>
        </FormatUrl>
        <FormatUrl InputUrl="$(_DeploymentComponentsUrl)">
            <Output TaskParameter="OutputUrl" PropertyName="_DeploymentFormattedComponentsUrl"/>
        </FormatUrl>
    </Target>

Note that the space only causes a problem with MAGE.exe, it does not cause a problem with MAGEUI.exe! MAGEUI.exe happily re-signs the release even with the space in the directory name. We cannot use MAGEUI.exe however because this process needs to be automated, if you use MAGEUI.exe someone has to run it and manually set the values…not good enough.

It took a while to figure out that the space in the dir name was causing a problem so I hope this saves someone else some time…

Creating a Release

Ok so now we have a published build that no one can use, the first place we want to release to will be QA, then from there UAT so what we need now is an automated way of creating a release configured for these environments.

Creating a release should be as easy as publishing a build, it should be a one step process.

Create a batch file ‘createRelease.bat’ which performs all of the necessary steps. Those steps are:

  1. Create or clean the ‘working’ directory.
  2. Copy the master build into the working directory.
  3. Run the MsBuild steps.
  4. Create a zip file of the new release. (not necessary just makes it a little easier to move the releases around)

Run the MsBuild Steps

Add a new target in your customized.targets file, ModifyMasterBuildForEnvironment.

<Target Name="ModifyMasterBuildForEnvironment" DependsOnTargets="SetPropertyValues; ConfigureForEnvironment; RecreateManifests" />

SetPropertyValues

There are a few properties which need to be set for the resigning process.

	<Target Name="SetPropertyValues" >
		<Message Text="FullReleasePath $(FullReleasePath)" />

		<MSBuild.ExtensionPack.Framework.TextString TaskAction="Replace" OldString="$(ApplicationVersion)" OldValue="." NewValue="_">
			<Output PropertyName="buildX" TaskParameter="NewString"/>
		</MSBuild.ExtensionPack.Framework.TextString>

		<PropertyGroup>
			<WorkingDirectory>WorkingDirectory</WorkingDirectory>
			<FullReleasePath>$(WorkingDirectory)\ApplicationFiles\$(AssemblyName)_$(buildX)</FullReleasePath>
			<ApplicationManifestName>$(AssemblyName).exe.manifest</ApplicationManifestName>
			<ApplicationManifestPath>$(FullReleasePath)\$(ApplicationManifestName)</ApplicationManifestPath>
			<DeploymentManifestPath>$(WorkingDirectory)\$(DeploymentManifestName)</DeploymentManifestPath>
			<CertFileName>cert\pmsKey.pfx</CertFileName>
			<ProviderFullUrl>$(ProviderBaseUrl)$(DeploymentManifestName)</ProviderFullUrl>
		</PropertyGroup>
	</Target>

ConfigureForEnvironment

Add your own target(s) here that does the work to configure the release for a given environment. e.g. update the config file setting database connection strings or web service urls.

RecreateManifests

Now that you have modified the published release it is no longer a valid ClickOnce release. Try to install it and it will fail. What you need to do is update and re-sign the manifests (the Application Manifest and the Deployment Manifest). You also need to update and resign the Setup.exe!

	<Target Name="RecreateManifests">

		<!-- Application manifest update and resign -->
		<Message Text="Removing .deploy suffix..." />
		<RenameFiles DirectoryPath="$(MSBuildProjectDirectory)\$(FullReleasePath)" RemoveSuffix=".deploy" IncludeSubDirectories="True" />
		<Message Text="Updating application manifest" />
		<Exec Command="mage -Update $(ApplicationManifestPath) -FromDirectory $(FullReleasePath) -ToFile $(ApplicationManifestPath)" />
		<Message Text="Adding .deploy suffix..." />
		<RenameFiles DirectoryPath="$(MSBuildProjectDirectory)\$(FullReleasePath)" AppendSuffix=".deploy" IncludeSubDirectories="True" ExcludedFiles="$(ApplicationManifestName)" />
		<Message Text="ApplicationManifestPath $(ApplicationManifestPath)" />
		<Exec Command="mage -Sign $(ApplicationManifestPath) -CertFile $(CertFileName)" />

		<!-- Deployment manifest update and resign -->
		<Message Text="Updating Deployment manifest (ProviderFullUrl=$(ProviderFullUrl))" />
		<Exec Command="mage -Update $(DeploymentManifestPath) -AppManifest $(ApplicationManifestPath) -ProviderUrl $(ProviderFullUrl)" />
		<Message Text="DeploymentManifestPath $(DeploymentManifestPath)" />
		<Exec Command="mage -Sign $(DeploymentManifestPath) -CertFile $(CertFileName) " />

		<!-- Setup.exe update and resign -->
		<Message Text="Updating $(WorkingDirectory)\Setup.exe..." />
		<Exec Command="$(WorkingDirectory)\Setup.exe /url=$(ProviderBaseUrl)" />
		<Message Text="Signing $(WorkingDirectory)\Setup.exe..." />
		<Exec Command="signtool sign /v /f $(CertFileName) $(WorkingDirectory)\Setup.exe" />

	</Target>

Note that before Updating the Application Manifest we have to remove the .deploy extension from the file names, then add the .deploy extension back onto the file names before Signing. (if you have configured ClickOnce to NOT append .deploy then obviously just remove these steps).

RenameFiles Task

To get the renaming behaviour I wanted I had to create my own MsBuild task. You will need to do the same, it’s a little crude as I had limited time but this is the simple little task I created….

    public class RenameFiles : Task
    {
        public override bool Execute()
        {
            var directory = new DirectoryInfo(DirectoryPath);
            if(directory == null)
                throw new ApplicationException(string.Format("Directory path is invalid: {0}", DirectoryPath));
            Console.WriteLine(string.Format("DirectoryPath:{0}", DirectoryPath));

            ProcessDirectory(directory);

            return true;
        }

        private void ProcessDirectory(DirectoryInfo directory)
        {
            RenameAllFiles(directory);
            if(! IncludeSubDirectories)
                return;

            var subs = directory.GetDirectories();
            foreach (DirectoryInfo sub in subs)
                ProcessDirectory(sub);
        }

        private void RenameAllFiles(DirectoryInfo directory)
        {
            var files = directory.GetFiles();

            if(files.Length == 0)
                return;

            foreach (FileInfo file in files)
            {
                string fileName = file.Name;
                if (ExcludedFileList.Exists(name => name.Equals(fileName, StringComparison.InvariantCultureIgnoreCase)))
                {
                    Console.WriteLine(string.Format("{0} is excluded from renaming.", file.Name));
                    continue;
                }
                Console.WriteLine(string.Format("Renaming {0}", file.Name));
                RemoveExtensions(file);
                AppendExtensions(file);
            }
        }

        private void RemoveExtensions(FileInfo file)
        {
            if (string.IsNullOrEmpty(RemoveSuffix))
                return;
            file.MoveTo(file.FullName.Replace(RemoveSuffix, string.Empty));
        }

        private void AppendExtensions(FileInfo file)
        {
            if (string.IsNullOrEmpty(AppendSuffix))
                return;
            file.MoveTo(string.Format("{0}{1}", file.FullName, AppendSuffix));
        }

        public string DirectoryPath { get; set; }
        public string RemoveSuffix { get; set; }
        public string AppendSuffix { get; set; }
        public string ExcludedFiles { get; set; }
        public bool IncludeSubDirectories { get; set; }

        private List<string> ExcludedFileList
        {
            get
            {
                if (excludedFileList == null)
                    excludedFileList = CommonFunctions.CreateListFromCommaSeparatedString(ExcludedFiles);
                return excludedFileList;
            }
        }
        private List<string> excludedFileList;
    }

Summary

There are a number of other little steps we do to make this release process work here but these are really specific to how we want it to work. What I have outlined above is the core steps involved including some problems you will face and their solutions. I hope this posting helps you get over the most challenging of the hurdles you will face in setting a smooth ‘Master Build’ release process but there is still alot of extra steps you will need to create yourself to get it working end to end.

One thing I would recommend is, once you have a working createRelease.bat (note it doesn’t have to be a batch file, could be an exe or powershell or whatever works best in your situation) then create a createReleaseALL.bat file. In there make n calls to createRelease.bat…one for each environment you need to create releases for. That way if you have a dozen environments you can still create a release for all of them with a one step process 🙂

I have had to do this fairly quickly so if something is not clear please let me know and I will see if I can improve the post over time.

Final note – It does take time to set this up and get it working correctly, but it is so worth it! Over time you will save much more time than you spent setting it up, guaranteed!

Additional Resources

A few links which helped me to piece this puzzle together…

ClickOnce with bootstraper setup.exe need to change URL without any build

packaging-a-clickonce-server-deployment

Avoid Data Loss – Version Upgrade with ClickOnce

Avoid Data Loss when Upgrading App Version

If you have an occassionally connected smart client, how do you ensure that your clients do not lose any data when they upgrade the application version? This posting has a tip about the UpdateMode property that helps with this question.

We use the Disconnected Service Agent (DSA) to help manage the occassionally connected scenarios. The DSA queues services calls in a local SQL Compact Database, the items in the queue get sent when a network connection is detected.

The Problem Scenario

But there is a problem with the following scenario:

1. User processes data in disco mode. Closes the application.
2. We put a new release on the server.
3. User re-connects to the network then starts the app.
4. ClickOnce advises the user that there is a new releases and asks if they want to upgrade or skip. (at this point there are queued service calls in the local database)

5. User chooses one of the following paths

Skip (good path)

– App starts with the old version
– Data captured in disco mode is sent to the server
– All good! n days later they will be prompted to upgrade their version again (define number of days in your .csproj)

Upgrade (bad path)

– Upgrade is downloaded from the server
– Local version is upgraded, which equates to a new install of the new release (and therefore a new local database)
– App starts with the new version
– Data captured in disco mode is lost, the new version has a new database.

So data loss can be avoided if the user chooses the right path, i.e. skips the upgrade the first time. But the last thing you want to do is rely on the user to make the right choice, if one path leads to data loss it’s really our fault as the developers for making the option available. So how do we take away the option?

ClickOnce UpdateMode

The problem scenario described above will only be an issue if you have set the UpdateMode property to ‘Foreground’.

UpdateMode may either be Foreground or Background. If it is set to Foreground the prompt above will appear when ClickOnce detects a new version and before the application starts. If it is set to Background, if ClickOnce detects a new version it:

  1. Starts the app with the currently installed version
  2. Downloads the new version, ready to go for the next restart.

Importantly what it means is that the first time they reconnect after working in disco mode, the upgrade will automatically be skipped, they’ll login with the old version and the captured data will be sent to the server. Then next time they start the app they will then be prompted to upgrade.

The ‘Background’ mode has the (minor) added advantage of the new version being downloaded in the background. In Foreground mode the user must wait for the download to complete when they click Upgrade.

So unless you have a very strong business case for it using Foreground, I’d recommend always setting the UpdateMode to Background!

Finding a Memory Leak in a .Net Application

Do You Have a Leak?

What do you do when you think your application has a memory leak?

I’m sure there are many options but this posting is about my experience with Red Gates ANTS memory profiler 5.1.

About Memory Management and the GC

There have been books written on this so I’m not going to attempt to explain. However I can’t stress enough how important it is to have at least a basic understanding about how the Garbage Collector works.

To compliment their products Red Gate have created a series of easy to understand on-line video’s about .Net Memory Management nicely broken down into several 5-10 minutes segments. These video’s are FREE so watch them!

If the video’s are not in depth enough for you, any article on the subject by Jeffrey Richter should satisfy your need for detailed explanations. Start with this one.

My Leak

Every time I opened and closed one of my controls, which was getting opened in a modal window, the memory used by the application increased and never went back down again. Which is weird because doesn’t .Net handle all memory management for me?

Do it does not.

So what is going on? The control in question has a dispose method, there’s nothing really tricky going on (no database connections etc) so how do I find out exactly what is causing the leak?

Finding the Leak

Enter ANTS memory profiler. I fire up the profiler, select my .exe and click ‘Start Profiling’.

Easy.

The app starts like normal but now the profiler has it under a microscope. Expect your app to run more slowly and make sure you have plenty of RAM.

I navigate to the screen where I can ‘launch’ the control that is leaking, then click ‘Take Memory Snapshot’.

Then I open and close the modal window say six times then click ‘Take Memory Snapshot’ once more.

Now we have two memory snapshots which we can compare. Stop Profiling for now, two is enough.

Click ‘Class List’, then in the grid click on the ‘Instance Diff (+/-)’ column header to sort by instance differences.

What we can clearly see now is a list of objects that are ‘alive’ in the program. What we care about is the difference in live instances from the first snapshot to the second. The ‘Instance Diff’ column provides this info. Now if we have a leak we should see an extra 6 live instances for our leaky control. So scroll down to the group of +6 items.

There are a couple of things of interest here in the profiler. First is yes we can see an extra six instance of our leaky control. Secondly, notice that ANTS puts your controls in bold. In this case we are looking for a specific problem but if you wanted to check for general memory leaks you can easily use the app for a while then scroll down until you see which of your objects is regularly increasing its instance count.

Ok great, we know it leaks, now what?

Dig a Little Deeper

Highlight the leaky object and click Instance List.

That will then show all 6 instance, just select one then click the ‘Object Retention Graph’ button.

In my case the graph appears empty, until I uncheck the ‘Hide’ box.

Now there’s some useful info…

In fact this tells me that there are several KeyEventHandlers that are stopping the GC from collecting my control.

Cool so back to the code and I add some code to unregister the handlers in the Dispose method.

_previousButton.Click -= PreviousButtonClick;
 _familyNameText.KeyDown -= Navigate;
 _givenNamesText.KeyDown -= Navigate;
...

Rebuild the solution and re-run the profiler. Take the initial snapshot, open close the leaky form half a dozen times, take the second snapshot.

Compare.

Now there are no more live instances of the leaky control! Our memory leak has been resolved.

Very easily!

Summary

It is almost certain that somewhere in your application there is a memory leak. With the right tool finding and resolving these leaks can be really easy, it just takes a little bit of time to familiarise yourself with the way the tool works.

The tools aren’t cheap but for any commercial application they are most definately worth the money. In this posting I used ANTS profiler from Red Gate, I am sure there are other tools out there that also do a great job, I just haven’t had the opportunity to use them. ANTS though is excellent (and no I do not have any association with them…beyond having bought a licence).

Events wired up to their handlers are often the ‘hidden’ cause of leaks. But even those of us who know how to manage memory in .Net often forget to do the right thing for whatever reason…usually time pressures. So I’d recommend explicitly building in some time into your project schedule to go hunting for memory leaks, find them before they go into production and everyone will sleep easier at night 🙂

ClickOnce – Exception reading manifest from file

the manifest may not be valid or the file could not be opened

This is a problem that you can run into when you publish an application using ClickOnce.

The error message provided by ClickOnce is a little obscure and there may well be a number of different causes, this posting is about what was causing the problem in my case.

This is a more complete look at the error

What Causes the Problem?

Multiple references to the same dll(s) (not necessarily just a project reference).

In our case we were pulling in non .net dll’s from two different directories that each contained about 5 dll’s that existed in both directories. Same version and everything, versioning was not causing the problem.

As soon as we removed the duplicates the problem went away.

We also experienced the same problem when we were pulling a configuration file explicitly from one location but had also added the file to the main project.  So once again because a file with the same name was ‘included’ in the publish, ClickOnce throws an error because the manifest is invalid.

This guy found the problem was due to an Enterprise Library dll. I don’t know the details but I’d be willing to bet that he had a reference to two different versions of the same dll in two different projects.

Summary

While it would be nice to get a clearer error message I think what ClickOnce is doing here is pretty nice. It is making sure that you don’t get into any trouble with different references to the same file…which could be a different version and give you a world of subtle, very difficult to find bugs.

I hope this makes sense…it has been written very quickly 🙂

ClickOnce – Creating Publish Page from msbuild

ClickOnce via msbuild

The Problem

When you publish from Visual Studio, the publish directory gets three files:-

  • Publish.htm
  • setup.exe
  • MyApp.application

but when you publish using msbuild you do not get the Publish.htm.

This post covers one approach to ensure you also get a nice publish.htm file. Actually it’s a two part blog, in the second part I’ll cover how to simplify your deployment process for multiple environments.

As a pre-requisite you’ll need to install msbuild community tasks.

All credit for this post belongs to my good friend Craig Hunter, this is pretty much a slightly modified version of his solution.

Step One – Create Customization File

This step in kind of optional. All subsequent steps may be done within your .csproj, it is just my preference to put all of my customizations into a separate file.

1. In your project file directory, create a new file called Customized.targets (actually call it whatever you want).
2. Now open your .csproj in notepad++, copy the first and last lines and add them to Customized.targets. Should look something like this

<Project DefaultTargets=”Build” xmlns=”http://schemas.microsoft.com/developer/msbuild/2003&#8243; ToolsVersion=”3.5″>
</Project>

3. At the bottom of your .csproj add these two lines

<Import Project=”Customized.targets” />
<Import Project=”$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets” />

What is not optional is adding the Import of the MSBuild community tasks.

Step Two – Add Publish Template

  1. In your project file directory, add a new directory called Publish.
  2. Add this file to that directory.

You don’t really need to but I also added the directory to my project. At the very least you do have to add it to source control (otherwise it won’t get to the build machine)

Step Three – Customizations

Add these customizations to your Customized.targets file. Remove the numbers, they are just there for reference below…

1.	<PropertyGroup>
		<BuildEnvironment>DEV</BuildEnvironment>
	</PropertyGroup>

2.	<Choose>
		<When Condition=" '$(BuildEnvironment)' == 'DEV' ">
		  <PropertyGroup>
		    <PublishDir>\\MachineIP\ReleaseDirectory\$(BuildEnvironment)\</PublishDir>
		  </PropertyGroup>
		</When>
		<When Condition=" '$(BuildEnvironment)' == 'QA' ">
		  <PropertyGroup>
		    <PublishDir>\\MachineIP\ReleaseDirectory\$(BuildEnvironment)\</PublishDir>
		  </PropertyGroup>
		</When>	</Choose>

3.	<PropertyGroup>
		<!-- Note this must be done AFTER the above Choose (so PublishDir is set)-->
		<PublishFilePath>$(PublishDir)publish.html</PublishFilePath>
	</PropertyGroup>

4.	<ItemGroup>
		<Tokens Include="PublisherName">
		  <ReplacementValue>$(PublisherName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ProductName">
		  <ReplacementValue>$(ProductName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ApplicationVersion">
		  <ReplacementValue>$(ApplicationVersion)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Prerequsites">
		  <ReplacementValue>@(BootstrapperPackage->'&lt;li&gt;%(ProductName)&lt;/li&gt;','%0D%0A')</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Username">
		  <ReplacementValue>$(Username)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
	</ItemGroup> 

5.	<Target Name="AfterPublish">
		<Time Format="dd/MM/yyyy HH:mm">
		  <Output TaskParameter="FormattedTime" PropertyName="PublishTime" />
		</Time>
		<!-- Finalise the publish.htm template file and copy it to the publish location -->
		<TemplateFile Template="Publish\publish.template.html" Tokens="@(Tokens)" OutputFilename="$(PublishFilePath)" />
		<FileUpdate Files="$(PublishFilePath)" Regex="\${PublishTime}" ReplacementText="$(PublishTime)" />
	</Target>

1. More on the BuildEnvironment property in a later posting. It’s not essential just remove all references to it if you want to remove it.

2. Also more on this in a later post, it is for specifying details for multiple environments. In the provided sample the PublishDir is the same machine and parent directory for both DEV and QA with a separating sub directory of the environment name. Don’t forget to SHARE the ReleaseDirectory folder!

3. Tells MSBuild where to put the publish.html file (name it what you want)

4. The publish.template.html has a number of tokens which need to be set.  Adjust these how you want, add or remove whatever tokens you want in the template.

5. After MSBuild finishes publishing it will run this section which creates the publish.html file using the token values.

Save those changes and you are done, you should now be able to run MSBuild, call publish and have a publish.html created for you with relevant information.

Step Four – Run MSBuild

Open Visual Studio command prompt, navigate to your project directory, execute (this assumes you only have one project file in the directory):

msbuild /t:Publish /p:Configuration=Release /p:BuildEnvironment=DEV

Done!

Why Bother?

If you get the file from VS why then care about what happens when publishing directly from msbuild?

I can think of three reasons why you need to publish from msbuild.

First, ideally developers should never publish from their machines.  (I say ideally because I have to raise a guilty hand and confess this is currently what I am doing…sometimes cirumstance forces bad practice upon us). This means ideally you have a build machine and it is from this machine only that releases should be done. So if you want to use VS to do the publish then you need to install and licence that machine.

Secondly, you want to be able to do a release from the command line, you don’t want to muck around starting VS, wait for the solution to load, open properties and click publish, what a waste of time, with msbuild you can create a batch file or two and start the publish process in the time it takes you to navigate to that directory.

Finally, the customization options available to you from within VS is too limiting. For example, if you are using VS to publish to multiple environments, how do you handle changing the parameters for each environment?

Some Notes

You can of course still run publish from VS but I don’t recommend it, get out of that bad habit and start using MSBuild directly. However if you insist on using VS, if you make any changes to Customization.targets, you need to unload and reload your project in VS for those changes to be ‘picked up’ by VS.

Please let me know if you identify any flaws or improvements that may be made with this approach.

<Choose>
<When Condition=” ‘$(BuildEnvironment)’ == ‘DEV’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecsport3.cps.com.au/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecsport3.cps.com.au/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edasport.cps.com.au:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecstest.cps.com.au/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\172.16.101.101\ReleaseMark\$(BuildEnvironment)\</PublishDir><!– InstallUrl>It seems that installUrl doesn’t get picked up if set here, must be fed in with the msbuild command arguments.</InstallUrl –>
<!– ApplicationVersion is set in the publishENV.bat –>
</PropertyGroup>
</When>
<When Condition=” ‘$(BuildEnvironment)’ == ‘SUB’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecspmsport1/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecspmsport1/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edas2-port1:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecspmshq/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\ecspmsport1\SmartClient\</PublishDir>
</PropertyGroup>
</When>
<When Condition=” ‘$(BuildEnvironment)’ == ‘DPS’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecspmsport2/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecspmsport2/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edas2-port2:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecspmshq/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\ecspmsport2\SmartClient\</PublishDir>
</PropertyGroup>
</When>
</Choose>

SCSF ‘Add View’ Recipe Problem

Smart Client Software Factory

We are using the SCSF on my current project and I have to say I like the idea but I hate the number of hoops you have to jump through in order to get it to work properly. One problem I recently experienced was with the Add View (with presenter)… recipe.

Add View (with presenter)…

The problem was it was not appearing in the context menu. So I looked in the Guidance Package Manager to see if I could glean any details about what I was doing wrong.

Guidance package manager

Guidance package manager

So as you can see the recipe applies to ‘any project referencing CAB libraries and the Infrastructure.Interface library.

SCSF has a solution template for creating SmartClient applications, which I had used and so I did have an Infrastructure.Interface project.

The specific ‘CAB libraries’ that the recipe cares about are:

  • Microsoft.Practices.CompositeUI.dll
  • Microsoft.Practices.ObjectBuilder.dll

So…to my project to check my references…

Project References

Project References

…hmmm, looks like all the references are good? Looks like this is one of those things that ‘should work but doesn’t’.

Debug Time

So, to get to the bottom of this, I had my solution open (VS1) and opened another instance of Visual Studio with the GuidancePackageManager.sln open (VS2). I then did an ‘attach to process’, attaching the process of the other visual studio.

attachtoprocess

attachtoprocess

Then find the right place in the GPM solution…here

breakpoint location

breakpoint location

then right click on a project in VS1, which causes the breakpoint to be hit in VS2.

The Cause of The Problem

Turns out that its happy with the CAB references but has a problem with the Infrastructure.Interface.

Why?

Because when the SCSF created the project it (quite correctly I think) created the Infrastructure.Interface project, setting the default namespace to ‘MySolution.Infrastructure.Interfaceand the assembly name to ‘MySolution.Infrastructure.Interface‘. But this means when it is comparing project references (in the ContainsReference method) if fails because it is looking for a reference to ‘Infrastructure.Interface‘ but finds ‘MyProject.Infrastructure.Interface‘.

To fix it we just went to Project Properties and in ‘Application’ set the Assembly Name back to Infrastructure.Interface. (we don’t expect to have any naming conflicts given the nature of the app).