Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Same source, multiple targets with different resources (Visual Studio .Net 2008)

A set of software products differ only by their resource strings, binary resources, and by the strings / graphics / product keys used by their Visual Studio Setup projects. What is the best way to create, organize, and maintain them?

i.e. All the products essentially consist of the same core functionality customized by graphics, strings, and other resource data to form each product. Imagine you are creating a set of products like "Excel for Bankers", Excel for Gardeners", "Excel for CEOs", etc. Each product has the the same functionality, but differs in name, graphics, help files, included templates etc.

The environment in which these are being built is: vanilla Windows.Forms / Visual Studio 2008 / C# / .Net.

The ideal solution would be easy to maintain. e.g. If I introduce a new string / new resource projects I haven't added the resource to should fail at compile time, not run time. (And subsequent localization of the products should also be feasible).

Hopefully I've missed the blindingly-obvious and easy way of doing all this. What is it?

============ Clarification(s) ================

By "product" I mean the package of software that gets installed by the installer and sold to the end user.

Currently I have one solution, consisting of multiple projects, (including a Setup project), which builds a set of assemblies and create a single installer.

What I need to produce are multiple products/installers, all with similar functionality, which are built from the same set of assemblies but differ in the set of resources used by one of the assemblies. What's the best way of doing this?

------------ The 95% Solution -----------------

Based upon Daminen_the_unbeliever's answer, a resource file per configuration can be achieved as follows:

  1. Create a class library project ("Satellite").
  2. Delete the default .cs file and add a folder ("Default")
  3. Create a resource file in the folder "MyResources"
  4. Properties - set CustomToolNamespace to something appropriate (e.g. "XXX")
  5. Make sure the access modifier for the resources is "Public". Add the resources. Edit the source code. Refer to the resources in your code as XXX.MyResources.ResourceName)
  6. Create Configurations for each product variant ("ConfigN")
  7. For each product variant, create a folder ("VariantN")
  8. Copy and Paste the MyResources file into each VariantN folder
  9. Unload the "Satellite" project, and edit the .csproj file
  10. For each "VariantN/MyResources" <Compile> or <EmbeddedResource> tag, add a Condition="'$(Configuration)' == 'ConfigN'" attribute.
  11. Save, Reload the .csproj, and you're done...

This creates a per-configuration resource file, which can (presumably) be further localized. Compile error messages are produced for any configuration that where a a resource is missing. The resource files can be localized using the standard method (create a second resources file (MyResources.fr.resx) and edit .csproj as before).

The reason this is a 95% solution is that resources used to initialize forms (e.g. Form Titles, button texts) can't be easily handled in the same manner - the easiest approach seems to be to overwrite these with values from the satellite assembly.

like image 542
MZB Avatar asked Mar 01 '10 19:03

MZB


Video Answer


2 Answers

You can add conditionals to elements within the MSBuild file. So for instance, if you have "Debug" resources and "Release" resources, you can place these within two separate folders (e.g. Debug and Release). Then, within your MSBuild file you might have:

  <ItemGroup>
    <Compile Include="Debug\Resource1.Designer.cs" Condition=" '$(Configuration)' == 'Debug' ">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resource1.resx</DependentUpon>
    </Compile>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="Queue.cs" />
    <Compile Include="Release\Resource1.Designer.cs" Condition=" '$(Configuration)' == 'Release' ">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resource1.resx</DependentUpon>
    </Compile>
    <Compile Include="Stack.cs" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="XMLFile1.xml" />
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="Debug\Resource1.resx" Condition=" '$(Configuration)' == 'Debug' ">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resource1.Designer.cs</LastGenOutput>
      <CustomToolNamespace>Resources</CustomToolNamespace>
    </EmbeddedResource>
    <EmbeddedResource Include="Release\Resource1.resx" Condition=" '$(Configuration)' == 'Release' ">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resource1.Designer.cs</LastGenOutput>
      <CustomToolNamespace>Resources</CustomToolNamespace>
    </EmbeddedResource>
  </ItemGroup>

Provided all of your access to your resources are via the Resources.Resource1 class, then you get two different sets of resources for debug and release builds. Obviously, this can be extended to further configurations.

Unfortunately, I don't think you can force the resources to use the same baseName (as provided to ResourceManager constructor), since it's based on the path within the project, and I can't find a way to override that. If you do need them to use the same name (if you're manually creating ResourceManagers, for example), then I'd suggest having a Resources1.resx (plus associated cs file) at the top level of the project, and not under source control. As a pre-build event, copy the required .resx file out from the Debug or Release directory as appropriate. (In this situation, you'd probably want to force it to not compile the .Designer.cs files within the subdirectories.

Edit

Forgot to mention (though it's seen in the above excerpt from the MSBuild file) that you have to set the Custom Tool Namespace on each .resx file to the same value (e.g. Resources), otherwise it also defaults to including the folder name.

Edit 2

In response to query about checking that each resource file contains the same resources - If you're using the Resource class (e.g. Resources.Resource1.MyFirstStringResource) to access your resources, then switching configurations will result in build errors if the required resource doesn't exist, so you'll find that quite quickly.

For the truly paranoid (i.e. if your build process takes 3 days to build all configurations, or something equally mad), at the end of the day, .resx files are just XML files - you just need something to check that each .resx file with the same filename contains the same number of <data> elements, with the same name attributes.

like image 178
Damien_The_Unbeliever Avatar answered Oct 31 '22 01:10

Damien_The_Unbeliever


Here is my solution. The original problem was that Autodesk releases every AutoCAD dlls with something different and breaks compatibility every 3 years. But our code is actually the same. That was a painful point in the way of Develop-TFS-TFSbuild-DomainControllerInstallationOfApps.

The solution is as follows :

  1. Make your MOTHER project with all the structure and, lets say the references for AutoCAD 2012.
  2. Then create second project, for AutoCAD 2013 in the same solution and reference its own DLLS.
  3. Now you have to set one compilation symbol for each project - acad2012 in the first project and acad2013 for the second.
  4. Then in the 2013 project go to Add Existing Item and point to the sources of the 2012 project BUT THEN CLICK on the right of the Add button and point to Add As Link
  5. If there is any library discrepancies between 2012 and 2013 you can surround them with #if acad2012 #endif or #if acad2013 #endif

That way you will have only one source code to maintain and have to separately compiled modules :)

UPDATE

What I need to produce are multiple products/installers, all with similar functionality, which are built from the same set of assemblies but differ in the set of resources used by one of the assemblies. What's the best way of doing this?

  1. Setting up the Installers projects : This is exactly what we do and exactly what you need. After step 5 you must make two installer projects. I mean Windows Installer Xml projects or WiX projects Lets say the first is for 2012 and one for 2013. The next problem is the generation of the XML files which contain all the components for the build. I use WiX toolkit with the following script :

    heat.exe dir "$(TargetDir)" -cg MyAppComponents -dr INSTALLFOLDER -sfrag -srd -var "var.FilesPath" -out ".\MyAppComponents.wxs"

This script builds the MyAppComponents.wxs with all the files you need. If you want to get acquainted with Wix go get this book : "WiX 3.6: A Developer's Guide to Windows Installer XML". I made everything needed after reading it.

  1. Add MyAppComponents.wxs to your main installer project directly (lets it be for 2012) and in any other project "Add As Link" (let it be for 2013 dlls). If you followed the instructions in step 5 then your build script is already made to compile against different versions of dependency assemblies. You want one source code compiled against two different dependencies right? This way you have it - one .wxs which contains everything and inside the .cs files you have pragmas which guide the compilation.

Now when you add new file to your solution you will need to add it in your Mother project then "add as link" to your other projects and add the component registration in .wxs only in one place in order to have it distributed in all the installers.

With the script from step 6 you will have all your versioned dlls inside one .wxs which mean they will be compiled in one single installer. Of course you can make several different installers for all your versions but it is simpler in the long run to make one single installer and to decide during installation which version of the .dll to copy to disk or to decide runtime which .dll to load in memory. I chose to decide runtime and it works fine for me.

  1. After your two install projects area ready you can use TFS Build for integration. You can go a step further and add an obfuscator like Smart Assembly in your build process so that at the end you have two or more separate installers with separately build dlls on different dependent libraries which contain obfuscated assemblies.

Everything described above is directly from trenches and it works. If I did not lay out all steps clearly just send me a message to clarify them and elaborate further.

I shifted away from Build Configurations because of the too much IFs and conditions in them that will accumulate with time when "yet another Autodesk dll appears". I am not very sure about this. Maybe it is a solution but you have to be very sure what you are doing in order not to brake the TFS Build if this is a requirement.

like image 39
Ognyan Dimitrov Avatar answered Oct 31 '22 01:10

Ognyan Dimitrov