Wix

Posted on January 9, 2009 
Filed Under .Net Code, CSharp, Windows Deployment, Wix, XML | 2 Comments

In finally get CruiseControl.Net set up to not only automate our build process, but notify us of any breaking changes on checkin, I discovered some issues in using the standard setup and deployment projects. Not wanting to spend a lot of time on it, I decided to look into Wix and see how it could help me streamline things.

It turns out Wix is a whole lot more work. Instead of a simple point and click interface, I’m presented with hand editing the XML file. Boo.

There are a few decent open source projects to give me a GUI interface so I can get my point and click back, but they turned out to either be for Wix 2 or they had some serious bugs in them.

In the end I used Dark to script my MSI file. While this worked out ok, Wix 3 wouldn’t compile the script without me making some changes. I dug through the script it created and I started to wonder how I was going to automate this. The trick is really not to automate it. The trick is to set up your script once and then leave it alone. Modifications are only made when things change in your build. That’s fine, but what about setting it up for the first time? You have to create a GUID for each component (read, every file if you want the files updateable) that gets installed. This can be time consuming if you have quite a few ancilary files that need to be shipped with the release.

Well, I automated that part. I created myself a small console application that will take a folder and a output file name as arguments. The program will traverse this folder and create an include script. It treats every file, directory, and directory of files as individual components.

In case anyone wants to do this but doesn’t want to take the time to write all the code, here it is.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Reflection;

namespace CreateIncludeScript
{
    class Program
    {
        static int Main(string[] args)
        {
            if (args.Length < 2 || args.Length > 2)
            {
                return (-1);
            }
            string folder = args[0];
            string outputfile = args[1];
            XmlDocument xmlDoc = new XmlDocument();
            XmlNode root = xmlDoc.AppendChild(xmlDoc.CreateElement("Include", "http://schemas.microsoft.com/wix/2006/wi"));

            XmlNode dir = root.AppendChild(xmlDoc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2006/wi"));
            SetAttr(xmlDoc, dir, "Id", "TARGETDIR");
            SetAttr(xmlDoc, dir, "Name", "SourceDir");

            ProcessDirectory(xmlDoc, dir, folder);

            XmlNode feat = root.AppendChild(xmlDoc.CreateElement("Feature", "http://schemas.microsoft.com/wix/2006/wi"));
            SetAttr(xmlDoc, feat, "Id", "DefaultFeature");
            SetAttr(xmlDoc, feat, "Level", "1");
            SetAttr(xmlDoc, feat, "ConfigurableDirectory", "TARGETDIR");
            ProcessFeatures(xmlDoc, feat, dir);

            xmlDoc.Save(outputfile);
            return (0);
        }

        static void SetAttr(XmlDocument doc, XmlNode node, string name, string value)
        {
            XmlAttribute attr = node.Attributes.Append(doc.CreateAttribute(name));
            attr.Value = value;
        }

        static void ProcessDirectory(XmlDocument xmlDoc, XmlNode target, string directory)
        {
            string[] files = Directory.GetFiles(directory);
            string[] dirs = Directory.GetDirectories(directory);

            foreach (string file in files)
            {
                FileInfo finfo = new FileInfo(file);

                XmlNode comp = target.AppendChild(xmlDoc.CreateElement("Component", "http://schemas.microsoft.com/wix/2006/wi"));
                string guid = System.Guid.NewGuid().ToString().ToUpper();
                string id = "C_" + guid.Replace("-", "");
                SetAttr(xmlDoc, comp, "Id", id);
                SetAttr(xmlDoc, comp, "Guid", "{" + guid + "}");

                XmlNode fnode = comp.AppendChild(xmlDoc.CreateElement("File", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, fnode, "Id", "F_" + guid.Replace("-",""));
                SetAttr(xmlDoc, fnode, "Name", finfo.Name);
                SetAttr(xmlDoc, fnode, "KeyPath", "yes");
                SetAttr(xmlDoc, fnode, "DiskId", "1");
                SetAttr(xmlDoc, fnode, "Source", finfo.FullName);

                if (finfo.Extension.ToLower() == ".dll" || finfo.Extension.ToLower() == ".exe")
                {
                    if (IsDotNetAssembly(finfo.FullName))
                    {
                        SetAttr(xmlDoc, fnode, "Assembly", ".net");
                        SetAttr(xmlDoc, fnode, "AssemblyManifest", "F_" + guid.Replace("-", ""));
                        SetAttr(xmlDoc, fnode, "AssemblyApplication", "F_" + guid.Replace("-", ""));
                    }
                }
            }

            foreach (string dir in dirs)
            {
                DirectoryInfo dinfo = new DirectoryInfo(dir);

                XmlNode dnode = target.AppendChild(xmlDoc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, dnode, "Id", dinfo.Name.Replace("-",""));
                SetAttr(xmlDoc, dnode, "Name", dinfo.Name);

                ProcessDirectory(xmlDoc, dnode, dinfo.FullName);
            }
        }

        static void ProcessFeatures(XmlDocument xmlDoc, XmlNode target, XmlNode root)
        {
            XmlNamespaceManager xmlmgr = new XmlNamespaceManager(xmlDoc.NameTable);
            xmlmgr.AddNamespace("wi", "http://schemas.microsoft.com/wix/2006/wi");
            XmlNodeList comps = root.SelectNodes("descendant::wi:Component", xmlmgr);

            foreach (XmlNode node in comps)
            {
                XmlNode compfeat = target.AppendChild(xmlDoc.CreateElement("ComponentRef", "http://schemas.microsoft.com/wix/2006/wi"));
                SetAttr(xmlDoc, compfeat, "Id", node.Attributes.GetNamedItem("Id").Value);
                XmlNode file = node.SelectSingleNode("wi:File", xmlmgr);
                if (file.Attributes.GetNamedItem("Assembly") != null)
                {
                    SetAttr(xmlDoc, compfeat, "Primary", "yes");
                }
            }
        }

        static bool IsDotNetAssembly(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                try
                {
                    using (BinaryReader binReader = new BinaryReader(fs))
                    {
                        try
                        {
                            fs.Position = 0x3C; //PE Header start offset
                            uint headerOffset = binReader.ReadUInt32();
                                                         fs.Position = headerOffset + 0x18;
                            UInt16 magicNumber = binReader.ReadUInt16();
                                                         int dictionaryOffset;
                            switch (magicNumber)
                            {
                                case 0x010B: dictionaryOffset = 0x60; break;
                                case 0x020B: dictionaryOffset = 0x70; break;
                                default:
                                    throw new Exception("Invalid Image Format");
                            }

                            //position to RVA 15
                            fs.Position = headerOffset + 0x18 + dictionaryOffset + 0x70;

                            //Read the value
                            uint rva15value = binReader.ReadUInt32();
                            return (rva15value != 0);
                        }
                        finally
                        {
                            binReader.Close();
                        }
                    }
                }
                finally
                {
                    fs.Close();
                }
            }
        }
    }
}

Make it easy for the IT department.

Posted on June 23, 2008 
Filed Under Windows Deployment | Leave a Comment

For quite a while I have noticed that there seems to be this huge divide between IT departments and the software developers. I say IT meaning the hardware/software support department and the programmers that write software. In my organization, we are only software developers. Any hardware support is done by me because I have had the most experience with it. Everyone else handles their own software issues. If they have an issue with Word or with their virus scanner of choice, they have to fix it themselves or ask questions to find out who knows what to do. Whenever I have had to call someone in IT at our customer’s location, we constantly are met with resistance and multitudes of questions that point to a certain mistrust of software developers. They almost treat us like we are Microsoft and are out to install a patch that will bring their entire system down for two days.

I start to gain a little insight on why it’s like this the other day. When we install our server components, which is solely SQL Server 2005 Express with a database including stored procedures and triggers, we have an installation package where you just have to click on setup.exe and let it do its thing. When I called the main contact at the IT department, I heard the hesitancy in his voice. I assured him he could do the installation if he preferred not giving me remote access to the server, but he insisted that I do it for him. No problem. Since we are customer focused, we always try to do the client and server installs of our software, anyway. I downloaded the files I needed and clicked on the setup program and just sat there while it went through the entire process. We chit-chatted about servers and Microsoft and had a few laughs over the new security requirements. He then described multiple instances where servers were down for a day or two because of patches that didn’t apply right and how he has had to call Microsoft dozens of times to get hotfixes or special patches because the combination of hardware and software they use is not normal. I usually do not get concerned when I hear this because we don’t usually care what they use, as long as Exchange Server is not on the same server as SQL Server.

After 15 minutes and clicking next a few times, our install was done. He said, “Ok, so what do we do now?” My response was simple, “Nothing. It’s all ready.” He was disbelieving and ran the client software and tried a few things to check. The next 10 minutes was eye opening. He described how this was the best install he has had to watch in a long time and was very pleased that we were so IT friendly. He described a situation that made me, as a software developer, cringe. They had purchased a brand new accounting system that cost their organization almost a hundred thousand dollars (US funds) and the installation took about 3 days. It took 3 days because they gave him the CDs and said to install it, but never gave him any instructions on how. Being used to installing software, he put in the CD marked #1 and ran the setup. It half way installed and then gave a half dozen error messages that consisted of things like “Error Occurs” and “Cannot Continue.” He tried called technical support and all he got was voice mails. He left messages but never received any phone calls in return.

He spent the next 3 days trying to solve the all problems himself, but with no documentation on requirements or procedures. When technical support finally called him back, the solution was to install the third CD first and then try the first CD. Why would you install a CD marked #3 before the CD marked #1? He told me that now he is highly concerned that they made the right choice because it’s their accounting system and how can they trust the vendor’s technical support department if he can’t get an answer in less than 3 days? What if they need to do payroll and “error occurs” and technical support doesn’t return phone calls?

This is why there is animosity between IT and software developers. Make it easy for them. Give descriptive error messages and most certainly answer your phone if it rings! Even if you know it’s a stupid problem they should be able to solve just by reading the screen, at least that phone call will keep their confidence that you will be there where there is a problem.

I <3 Linq

Posted on June 3, 2008 
Filed Under linq | Leave a Comment

One of the latest technologies to hit .Net recently is Linq. Language Integrated Query allows you to perform a more SQL like syntax on object collections in code. It has been out for quite some time, but I just recently was able to use it in a project. It’s pretty cool stuff and has already saved me a few lines of code. Places where loops would have been neccessary have easily been replaced with a simple where.

One of the concerns most developers had, especially when working with Linq to SQL data classes, was the relative speed at retrieving data from the SQL server. They were pretty right about that. It’s slow. Much slower than even using datasets with table adapters. However, using compiled queries can help.

One thing some people never took into consideration was the relative ease that Developer’s Express users can now use server mode without having to use the proprietary XPO classes. Their XtraGrid’s now support ServerMode which will page SQL queries in windows forms applications. One issue we had were slow VPN clients trying to use a windows form application installed locally and connecting to a remote SQL Server. Until recently we have had to tell users to use terminal services instead. Some forms have to show a relatively large amount of data in a single grid (finding customers, paging through certain data, etc). Over a VPN connection it was just painfully slow. Now combining XtraGrid’s and the Linq Server Mode, everything works as expected over a slow (1.5 MB DSL even) VPN connection.

TableAdapter Connection Strings

Posted on February 18, 2008 
Filed Under .Net Code, CSharp, VB.Net | 23 Comments

* 7/29/2010 Update – I have uploaded a sample project here for those of you still struggling with this. It includes C# and VB.Net projects. Enjoy!

I don’t know how a lot of people handle it, but one thing that always bugged me about table adapters and datasets is the way it handles it’s connection strings. It starts out innocent enough. A new dataset is created with it’s associated table adapter. The connection string is saved to the application settings file and that property is saved in the dataset. Running it on the development machine (or on the same network) is no big deal and just works. But what if you send that application to someone else that has their own SQL Server?

Did anyone at Microsoft actually use this scenario in a production environment? What were they thinking?

In order to run that in a production environment you have to set the connection string in the app.config file. While this may be fine for some people, what about the people like me that do not want my users to access that database? I don’t want them to have that username and password… While the chances of a normal user loading SQL Server Management Studio and logging in are slim, it’s still possible and it’s definitely possible if a user purposely wants to get out of having to do work that day.

What are the options? Well, one option is to use the encryption to encrypt the settings in app.config. For me, this option is not ideal. Reports of those settings getting corrupted are quite high, plus you have to deal with the loading and saving of those settings, which isn’t all that easy to do.

The other option is to take the route I was taking for a while… I had a function that would build me a connection string. Then I could: TableAdapter.Connection.ConnectionString = myLibrary.ConnectionStringFunction()

This was great, until that day came where I was in a hurry and added a few more tables to a form but forgot to set the ConnectionStrings. Whoops.

So I needed a solution that would stop me from having to set those ConnectionString properties, keep my connection string out of the app.config, and be easy to use (i.e. Just Works).

I started out by just giving all my datasets the same connection string. Then on application startup, I tried to change that one application setting. Hmm.. It seems those ConnectionString properties are set to friend and are read only.

Upon further investigation, it seems that there are some events that fire, such as SettingsLoaded. This event fires when the app.config is read and all the settings are loaded. When this event fires, it fires inside the MySettings class. This should allow that property to be changed.

    Private Sub MySettings_SettingsLoaded(ByVal sender As Object, ByVal e As System.Configuration.SettingsLoadedEventArgs) Handles Me.SettingsLoaded
      Me.Item("MyAppConnectionString") = MyLibrary.BuildConnectionString()
    End Sub

This will set the MyAppConnectionString setting to the proper connection string. Now, all table adapters will have an up to date connection string.

So what happens if you want to change the connection string later while the application is still running? Well, there is no way to do that. So it’s time to come up with a way to trick it into updating that property.

In looking at the MySettings class, there is another event called PropertyChanged. We can use this event if we create another setting that can be updated anywhere in the application. First, we create a new string setting that has a User scope (I called mine ConnectionString). This will allow the application to update the setting at any time.

Next, we need to create a function that will update that property with our connection string.

  Public Shared Sub ChangeConnectionString()
    My.Settings.ConnectionString = BuildConnectionString()
  End Sub

Now we can change the events in the MySettings class to look like this.

    Private Sub MySettings_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles Me.PropertyChanged
      If e.PropertyName = "ConnectionString" Then
        Me.Item("MyAppConnectionString") = My.Settings.ConnectionString
      End If
    End Sub

    Private Sub MySettings_SettingsLoaded(ByVal sender As Object, ByVal e As System.Configuration.SettingsLoadedEventArgs) Handles Me.SettingsLoaded
      MyLibrary.ChangeConnectionString()
    End Sub
  End Class

Now, every time that ChangeConnectionString() is called, the MyAppConnectionString will be updated. The ChangeConnectionString procedure can be changed so that it can accept a string parameter that is the actual connection string. Then you can build a Connection String anywhere and just pass it to that procedure.

← Previous PageNext Page →