Thursday, July 26, 2007
Jeremy Miller's build your own CAB series
I've been really enjoying Jeremy Miller's build you own CAB series of blog posts. He gives a great introduction to good fat client UI programming techniques. You hear plenty about MVC and MVP, but it's rare to get such a detailed introduction with lots of code.
Thursday, July 19, 2007
How to structure Visual Studio solutions
Because I work as a freelancer, I get to see a lot of different .NET development shops. One of the things that continually surprises and frustrates me is how poorly many teams organise their solutions. By this I mean the way they split their application into projects (and thus assemblies), the way they group those projects into solutions, naming conventions and the way they source control those solutions.
Microsoft’s Patterns and Practices group has specific guidance about how you should organise your solutions here and you can’t go too badly wrong by following their advice. Unfortunately some of the following articles about build practices are way out of date and really should be updated, but that’s for another post, right now I want to give a brief list of do and don’ts on organising solutions.
The most important thing to get right is to understand your team and the systems it builds and maintains. What source is in your control and what is outside it. It’s very important that you don’t build artificial silos within your team that make sharing and reusing code difficult. Ideally, all the source your team writes should be in a single solution file with all the projects referenced with project references. Not doing this is the single biggest source of solution problems I’ve seen. Never use file references for internal assemblies! It’s such a headache making sure you’ve got the correct version of an internal assembly especially when it’s busy being developed.
I’ve often seen the situation when two projects in the same solution both file reference the same internal assembly, but different versions of it so when you build the solution you get errors because visual studio complains that it will have to overwrite one version of an assembly with another. Also there’s the issue of where you reference those assemblies from. If you reference the build server’s (you do have a build server right?) latest built assemblies, you can often find that your local build breaks when another developer checks in and builds a new version of the assemblies that you’re referencing. On the other hand I’ve worked on teams where I’ve had to manually maintain assemblies and file references. All this is bad and unnecessary.
When I arrive to work with your team I should be able to get the latest code from source control open the solution file and hit F5 and the build should work first time. Never ever put your stuff in the GAC. The only excuse is if you are forced to by COM interop issues. It’s always a bad decision and will give you endless build and deployment nightmares.
What about third party assemblies you reference, or assemblies from other parts of your company that your team has no control over? If it’s a .NET API to a non .NET product and you’re going to have to run its installer on your deployment target and the installer places the assembly in the GAC, then it’s probably best to just go with that. For pure .NET assemblies that can be xcopy deployed, the best think is to treat them like other binary resources and put them in your repository along with the source. You can either put them in their own solution folder or include them in each project that references them as a project item (as suggested by the P&P document above). The only problem with the latter approach is that it can be a headache when you want to move to a newer version and you have to hunt down all the projects that reference it.
OK, so we have a single solution with all the team’s projects in it. How do we name and organise those projects within a solution? As far as naming goes there’s a simple rule that will make your life much much easier: Project Name = Assembly Name = Assembly File Name = Root Namespace = Project Folder Name.
For example, say you’ve got a root namespace like this: MyCompany.MyApplication.DataAccess, then the project name should also be: MyCompany.MyApplication.DataAccess, in a folder called: MyCompany.MyApplication.DataAccess. The assembly name should be: MyCompany.MyApplication.DataAccess, and the assembly’s file name should be: MyCompany.MyApplication.DataAccess.dll
I would expect the solution name to be MyCompany.MyApplication or maybe MyCompany.TeamName if your team’s solution file holds a number of different applications. You do share code between your team’s apps don’t you? On disk you should keep things flat. The solution should go in a folder with the same name as the solution (MyCompany.MyApplication) and all the projects should go in child folders of the solution folder. This is how VS likes it and it’s pointless fighting the power.
One place where you do want to fight the power is with web projects. Don’t let VS put them in a virtual directory under WWWRoot. Create a blank solution file first, then create a folder under the solution directory for your web project. Create a virtual directory using inetmgr that points to the project folder. Last of all, create the web project and ask visual studio to put it in the virtual directory you just created. This is much easier with VS 2005 + and the problem has mostly disappeared because you can use the cassini web server to run a web project from wherever it happens to be on disk.
Your source repository hierarchy should exactly match the solution structure on disk. Not doing this is a recipe for disaster. Don’t be tempted to use the file linking feature in Source Safe to share source files between different solutions, it causes endless headaches whenever someone adds a new source file to a project, checks the project file in but doesn’t add the new file to every location the project’s linked to. As for source safe, although it’s the default option for every Microsoft shop, it’s also probably the worst SCM tool out there. Really consider using something more modern. I haven’t had the opportunity to use the new Team System source control tool, but I have used Subversion on one project and it was like moving from a Trabant to a BMW. However that one experience with Subversion was the only one in my long career as a Microsoft developer, everyone else uses Source Safe. A great pity and definitely a subject for a future post.
Wednesday, July 18, 2007
Serializing lots of different objects into a single file
Here's a neat trick I discovered a while back that I thought I'd share. Us .NET programmers are always doing serialization for one reason or another. The built in BCL binary serializer, System.Runtime.Serialization.Formatters.Binary.BinaryFormatter is a really easy way of persisting objects to disk or any other kind of binary stream. What I didn't realise until I discovered this trick is that you can serialize one object after another onto a single stream and then read them back one by one. You can create a file, serialize some objects to it, close it, then open it again and append a few more. Also the objects don't have to be the same type, the BinaryFormatter just reads to the next object boundary and then returns the object cast as object. Also you don't have to read all the objects back into memory at once. So long as you remember the position of the last object you deserialized, you can just continue at some later date. This is really efficient if you've got huge collections of things you want to store and process.
Of course if you want to serialize a lot of independent objects (or object graphs) you could always insert them into some data structure like an ArrayList and then serialize the ArrayList, but this means creating all the objects in memory at once and reading them all back into memory at once which is fine with small collections, but isn't a good strategy for larger amounts of data.
Here's a little demo. The meat of it is the functions WriteAnimalToFile and ReadAnimalFromFile. WriteAnimalToFile opens a file, writes one Animal object to it and then closes the file. In the demo we do this for 10 different types of Animal, note that Animal is an abstract base class that's specialized by Cat and Dog. ReadAnimalFromFile opens a file, seeks to the given position, reads one animal back and then closes it. It returns the animal and the new position. In the demo we read back all the animals we created with WriteAnimalToFile. Note that in ReadAnimalFromFile we don't have to tell the BinaryFormatter what kind of object to expect, it just reads to the next object boundry. If the position is at the end of the file, we just return null.
using System; using System.IO; using NUnit.Framework; namespace SerializerTest { [TestFixture] public class SerializerTests { [Test] public void SerializeLotsOfObjects() { // get the path for the file we're going to serialize into string path = @"c:\SerializedObjects.ser"; // create the file we're going to use using(File.Create(path)){} // create some animals for(int i=0; i<10; i++) { Animal animal; string name = string.Format("Animal_{0}", i); int age = 5+i; // make even numbers dogs, odd numbers cats if((i % 2) == 0) { bool trained = ((i % 3) == 0); animal = new Dog(name, age, trained); } else { int lives = 9-i; animal = new Cat(name, age, lives); } // write each animal to a file WriteAnimalToFile(path, animal); } // read the animals back one by one. long position = 0; while(true) { Animal animal = ReadAnimalFromFile(path, ref position); if(animal == null) break; Console.WriteLine(animal.Introduce()); } } private void WriteAnimalToFile(string path, Animal animal) { // create a new formatter instance System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); // open a filestream using(FileStream stream = new FileStream(path, FileMode.Append, FileAccess.Write)) { formatter.Serialize(stream, animal); } } private Animal ReadAnimalFromFile(string path, ref long position) { // create a new formatter instance System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); // read the animal as position back Animal animal = null; using(FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { if(position < stream.Length) { stream.Seek(position, SeekOrigin.Begin); animal = (Animal)formatter.Deserialize(stream); position = stream.Position; } } return animal; } } [Serializable] public abstract class Animal { string _name; int _age; public Animal(string name, int age) { _name = name; _age = age; } public abstract string Introduce(); public string Name{ get { return _name; } } public int Age{ get { return _age; } } } [Serializable] public class Dog : Animal { bool _isTrained; public Dog(string name, int age, bool isTrained) : base(name, age) { _isTrained = isTrained; } public override string Introduce() { return string.Format("I am a dog called {0}, age {1}, {2}trained.", Name, Age, (_isTrained ? "": "not ")); } public bool IsTrained{ get { return _isTrained; } } } [Serializable] public class Cat : Animal { int _lives; public Cat(string name, int age, int lives) : base(name, age) { _lives = lives; } public override string Introduce() { return string.Format("I am a cat called {0}, age {1}, with {2} lives", Name, Age, _lives); } public int Lives{ get { return _lives; } } } }The output should look like this:
I am a dog called Animal_0, age 5, trained. I am a cat called Animal_1, age 6, with 8 lives I am a dog called Animal_2, age 7, not trained. I am a cat called Animal_3, age 8, with 6 lives I am a dog called Animal_4, age 9, not trained. I am a cat called Animal_5, age 10, with 4 lives I am a dog called Animal_6, age 11, trained. I am a cat called Animal_7, age 12, with 2 lives I am a dog called Animal_8, age 13, not trained. I am a cat called Animal_9, age 14, with 0 livesNote that you can't do this trick with the XML Serializer since we have to specify the type we're expecting. Also a single file with multiple XML documents would be malformed.
Subscribe to:
Posts (Atom)