Friday, December 29, 2006

ClickOnce - Very Cool But Mostly Useless (At first glance)

I recently started playing with Microsoft's ClickOnce framework that was introduced in .NET 2.0. I had my own auto-update mechanism, but it's still pretty clunky and not as good as I wanted it to be, and I wanted to have a real install. When I came across ClickOnce it looked very impressive.
  • Auto-updating when a new version is published
  • Revert to previous version
  • Start menu and Add/Remove program support
  • Side-by-side execution
  • Auto generation of a web page for users to easily install your app
  • Ability to create "Groups" that can be installed on demand
That sounds perfect! Just what I'm looking for! After a little reading, I thought I'd give it a try. I created a test Windows Forms application and a dependent assembly. I enabled IIS on one of my computers so I had a place to publish to. Publishing is literally as easy as going to the project settings, selecting the "Publish" tab and clicking the "Publish" button.

My machine cranked for a bit, the files were transferred to the web server and the "Publish" page opened in my default browser. I click the "Install" button and I immediately come across the first problem - the install fails.

Bad thing #1: ClickOnce is not designed to work with any browser except Internet Explorer!

Not too big of a problem, I guess, as long as I'm aware of it and make note of it on my web site. After switching to IE and clicking on the "Install" button, it does some stuff and I have a Start Menu icon for my test program. I was anxious to test the auto-update feature. I made a small change to the main program and published again. The Publish feature of VS.NET 2005 is even nice enough to automatically bump the version number every time you publish!

With the new version in place on the server, I run my test program, the bootstrapper tells me there is an update available and ask if I want to install it. I choose to install the update and sure enough, the app has been updated. I can barely contain my excitement - this is so cool! I can't wait to test this with my [as of yet unnamed] program.

After going to the Publish tab in the project settings of my program, I check the files list and immediately notice that all of my plugins are missing! I look for a way to add the missing files manually, but don't see a way to do this in the UI. The first screen shot below shows the files in my output. I was expecting the "Plugins" directory and files below it to be picked up. Or, at the very least have a way of adding them by hand in the "Application Files" dialog (second screen shot below).


Bad thing #2: You can't add files that aren't directly referenced by your projects (or as far as I can tell, create an arbitrary directory structure)

My program uses plugins that are discovered and used at runtime via reflection. They live in a subdirectory of the main application folder and ClickOnce does not appear to include a way of creating subdirectories and placing files not directly referenced by the project. One thing I haven't tried is adding a reference to the plugin projects and setting the option to not copy to the output directory. I do the copy as a post-build step.

Bad thing #3: Where the heck are the files installed?

ClickOnce installs your program into a subdirectory of your "Documents and Settings/Local Settings/Apps". The full path is truly hideous and is different on everyone's machine. This is by design. In order to support the rollback feature, Microsoft wants control over the path to avoid name collisions and to make sure people don't install to parts of the file system that may require elevated permission to write to, like the "Program Files" folder. That said, if I wanted to tell a user to send me his settings file, how do I tell them where to get it from? Here's the full path on my system:

"C:\Documents and Settings\[me]\Local Settings\Apps\2.0\2JD5D4H3.AB3\5CWBKLHB.QNL\unna..tion_60261a46dfa9d898_0000.0002_713c372ded58c204"

Ain't that ugly? When I first tried to find where the application was installed, I went to the shortcut that was created in my Start Menu and looked at the properties. It *looks* like a normal shortcut, has the shortcut overlay, but doesn't appear to be pointing at anything.

So, it took a little bit of digging to find where ClickOnce put the files. Luckily, .NET provides an API to get the install location, so at least there is a way to provide access to my settings file in the program itself via a menu option, for example.

This is all truly disappointing. No control over where the files go and no way to add plugins, directories or other unreferenced files to the install set. And they're marketing this as a solution for deploying thick client/smart client apps? Unless you have a very simple app where everying lives in the same directory, you pretty much have to use something else. I don't know of many applications that fall into that category, unless they are corporate utilities or something like it.

I'd love to know how to do these things if you can, but from a couple days of reading and experimentation, I didn't see anything that would make me believe it could. If anyone reading this knows more, I'd love to hear about it!

Update: I did just find a tool that comes with VS2005, called MageUI, that seems to allow you to point at a directory and have it pick up all the files recursively. There looks to be a command-line utility as well that, I assume, can be used with MSBuild to do what I want. But, why didn't Microsoft just add the ability to do some of this in the UI built into VS2005?

Update 2: Found this article which provides information on how I can accomplish what I need to do. Again - why didn't Microsoft put that one extra feature that is so obviously needed into the VS2005 project settings!? Sheesh!

Tuesday, November 21, 2006

Maintaining Window State

Remembering the window size and position between sessions sounds easy enough, doesn't it? I thought it would be, too until I tried to do it. Things get more complex when you offer the ability to "hide when minimized" and have a system tray icon. One of the first problems is when the application is shutdown with the window hidden. I wanted the app to start in that state, but also remember the last known position and size of the main window. I also wanted to delay creation of any windows until they were needed. In other words, if the app was shutdown with only the system tray visible, it should be able to start up in that state and not have to create any of the UI until it is needed.

After several rounds of trial and error, I settled on a "WindowStateManager" class that adds event handlers for window size and position changes, as well as saving them to settings. In addition, this class would be responsible for knowing when to create the main window and would fire an event when the window was created. The application entry point class listens to that event so it can do whatever it needs to do when the main form is created. This is a departure from what the typical Windows Forms application looks like in that the Application.Run method doesn't receive the MainForm instance.

So, here's the application entry point:


[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.ThreadException +=
new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
_splash = new AppSplash();
_splash.Closed += new EventHandler(splash_Closed);
_splash.Show();

initApplication();
startSystemTrayIcon();
Application.Run();
}


First the usual stuff generated by Visual Studio. Then, it registers some top-level exception handlers, deal with the splash screen (I'll post my implementation of that in a later post). That's followed by a call to "initApplication()", which at the moment deals with the WindowStateManager and the creation of the tray icon. Finally, Application.Run() is called to start running the application. The main window may or may be created depending on the how the app was shut down. If it was shut down with the main window visible, it will be created by the WindowStateManager. Otherwise, no UI is created at all.



private static void initApplication()
{
_windowStateManger.MainFormCreated += new WindowStateManager.MainFormCreatedHandler(windowStateManager_MainFormCreated);
_windowStateManger.init();
}

The WindowStateManager.init() method checks the settings that were read from disk, and then creates the main window if necessary:


public void init()
{
GlobalSettings global = AppSettings.Instance.Global;
if (global.MainWindowState != FormWindowState.Minimized)
{
createMainForm();
}
}
"createMainForm()" creates the MainForm instance, fires the "MainFormCreated" event, registers event handlers for the "SizeChanged" and "PositionChanged" events, and sets the size and state of the window.


private void createMainForm()
{
Debug.Assert(_form == null);
createTheForm();
registerEvents();
restoreWindowState();
}

private void createTheForm()
{
_form = new MainForm();
if (MainFormCreated != null)
{
MainFormCreated(_form);
}
}

private void restoreWindowState()
{
GlobalSettings global = AppSettings.Instance.Global;
if (global.MainWindowSize.IsEmpty)
{
_form.DesktopBounds = getDefaultDesktopBounds();
}
else
{
_form.Size = global.MainWindowSize.Size;
_form.Location = global.MainWindowLocation;
}

_form.WindowState = global.MainWindowState;
if(_form.WindowState != FormWindowState.Minimized)
{
_form.Show();
}
}

private void registerEvents()
{
_form.SizeChanged += new EventHandler(sizeChanged);
_form.LocationChanged += new EventHandler(locationChanged);
}

private void sizeChanged(object sender, EventArgs e)
{
if (_form.WindowState != FormWindowState.Maximized && _form.WindowState != FormWindowState.Minimized)
{
_lastSize = _form.DesktopBounds;
}
else if (_form.WindowState == FormWindowState.Minimized)
{
_form.Visible = false;
}
}

private void locationChanged(object sender, EventArgs e)
{
if (_form.WindowState == FormWindowState.Normal)
{
_lastLocation = _form.Location;
}
}

I'm not sure if accessing the AppSettings directly from within this class is the best thing to do (perhaps the relevant data should be passed in?), but it works. I may change that yet. At least now, the application starts in exactly the same state and position it was when it was shut down. This even works on multimonitor configurations, although, I didn't test it with the second monitor on the left.

Sunday, November 19, 2006

Settings and Persisting Window State

The next thing I hooked up was the settings. I've seen a number of ways to do this, and I decided to use the XML serialization services build into .NET. It's simple, although somewhat slow. Still, it's very easy to understand and can be utilized with just a few lines of code. Basically, it takes a data type object and generates an XML file from the read/write properties of the class. I have one class per logical group of settings which translates to one file per class: "Network", "Lan", "Global" and "Other". Around that, I have a singleton instance called "AppSettings" that acts as a container for the different settings classes. The AppSettings class is what the application interfaces with and is where the saving and loading of the program settings happens as the application is started and shut down.

When the AppSettings instance is created, the private "init" method is called. This checks if the "Settings" directory is there and creates it if it isn't. Then, it loads the settings.


///
/// Initializes the settings path and creates the "Settings" directory if it doesn't exist.
///

private void init()
{
_fFirstTimeLoad = false;
_strSettingsPath = Path.Combine(Environment.CurrentDirectory, SettingsDirectory);
if (!Directory.Exists(_strSettingsPath) )
{
_fFirstTimeLoad = true;
Directory.CreateDirectory(_strSettingsPath);
}

// Or if there's no files in the settings directory, treat it like a first time run
if (Directory.GetFiles(_strSettingsPath).Length == 0)
{
_fFirstTimeLoad = true;
}

loadSettings();
}


The loadSettings() method just calls the static "load" method on each of the components of the settings.



  private void loadSettings()
{
_globalSettings = GlobalSettings.load(_strSettingsPath);
_networkSettings = NetworkSettings.load(_strSettingsPath);
_lanSettings = LanSettings.load(_strSettingsPath);
_otherSettings = OtherSettings.load(_strSettingsPath);
}


Each of the settings classes derive from a base class that handles the XML serialization to and from disk.

In a nutshell it opens a FileStream, creates a serializer of the correct type and then calls Deserialize. Viola! Your settings are restored! Here's the guts of it. I removed the exception handling code just to keep it brief here.




protected static AbstractSettingsItem read(Type type, string strPath)
{
FileStream fs;
fs = File.Open(strPath, FileMode.Open, FileAccess.Read);
XmlSerializer xs = new XmlSerializer(type);
object objData;
objData = xs.Deserialize(fs);
fs.Close();
return (objData as AbstractSettingsItem);
}

The code to save the settings is very similar except, "Serialize" is called instead. This makes for a very easy way to blast settings to disk and restore them later. Provided all settings are initialized with proper defaults, adding new settings are accommodated easily as the program grows. I don't know if this is the best way of doing this, but it seems to work pretty well.

I think one way to improve it would be to extract an interface to pass to and from the subclasses and the base class, so that reading doesn't pass back an object of type "AbstractSettingsItem" - it just seems awkward to me. Not sure what methods would be defined in the interface, though. I'll have to think about that more.

For now, if I want to add a new settings class, I just derive from "AbstractSettings", add the data members to hold the settings data, and write the properties that are to be saved and restored between sessions. Then add an instance to the AppSettings class and add the calls to "load" and "save".

I realized as I was writing this, that the current implementation is pretty much limited to simple types: boo, int, string, etc. The default XML serialization handles arrays and collections pretty well, and also nested objects, as long as they have public read and write properties. Beyond that, I think I'd have to provide a custom serializer or handle writing out my own XML document. For the most part, I think settings fall into the "simple types" category. At least I haven't run into anything that required something more complex.

Next up, saving and restoring the main window size and state with a system tray icon.

Wednesday, November 15, 2006

Starting Fresh

I was converting the Game-Tracker project to the .NET 2.0 framework to make use of the new project structure and better resource handling. Even though it's not necessary, I wanted to convert the forms so they had the seperate Designer classes and resources. Pretty much the only way to do that (that I know of) is to create a new form, and then copy the code over from the 1.1 form. So far so good. I create a new project, create a new MainForm and start carrying over code. In the process, I'm thinking about all the stuff I did wrong and don't like about Game-Tracker. First of all, I realize I don't like the name. Time for a new name. Also, I think about all the crap that I don't like about the source code. Since I was learning .NET/C#, I did a lot of experimenting and made a lot of mistakes, having to re-do parts of it. Parts of it were terribly confusing and difficult to work in. Hmm.

Time to scrap the old and start fresh! w00t! It's one of the benefits of being the only developer on a free project that you do in your own time that has never been released. :-) That freed me up to think about what I didn't like about Game-Tracker and either remove it or fix it. One thing was that the UI was plain and somewhat cluttered - one of the first things I didn't like about the GameSpy UI. I really like a clean UI and an app that does one thing well. What I would like this app to do well, is monitor and find public Internet game servers. And, like everyone else, I want a slick, nice-looking UI.

I think the game protocol plugins are still good, as are a number of the non-UI components. The game template and networking code is ok. I still needed to optimize the networking code a bit, so that when querying a lot of servers the data gets pulled off the socket as quickly as possible. I can also carry over the multicast chat utility, which already works great on a lan. I plan to extend this to a true "friends" type feature. So, quite a bit is reusable and "good", in my opinion. (I still have my doubts about using a blocking socket call in the plugins, but I'll talk about that in a different post)



I created a new project in VS2005, created the main window and started with a simple ToolStrip at the top and StatusStrip at the bottom. I won't be using the .NET 1.1 Menus or StatusBars at all. The new ToolStrip components are much more flexible, and just look a lot nicer. As a plus, the ToolStrip components use a bit of the .NET 3.0 component structure with their "Renderer" property, so it seems like a good choice moving forward. I also started a custom control to handle the display of the game icons installed on the users' system. I have a rough idea of how I'd like it to loook, but we'll see how it turns out. Here's a screenshot of what I have so far. Not very interesting, but with a little imagination, I think you'll see what I'm striving for.



The red squares at the top will be where the game icons are. When the app starts up for the first time, it reads the templates I've defined for the games that it has support for, scans the user's system looking for the games, extracts the appropriate icon and will display them in that control at the top. That's the idea any way. I have to admit, I've never had the "gift of great UI", so any suggestions/feedback will be listened to. I know that pretty much everyone is better at it than me! :-)

Tuesday, October 24, 2006

To code, or not to code ...

I've been trying to finish up a number of programming books that I've started reading. One of which is "Extreme Programming Adventures in C#" by Ron Jeffries. It's a different kind of programming book. It's essentially a journal of Ron's experience learning the C# programming language while applying Extreme Programming principles. He discusses the assumptions he's making, you see the first (sometimes ugly) attempts he makes writing code. What's good about it, is that he goes back and fixes the code that's bad, so you see the code evolve instead of just seeing the final product.

At the end of one of the chapters, he discusses a philosophy of XP which basically says, "don't put any code in until it's needed". I keep reminding myself because I tend to want to program the world into an app instead of moving in small pieces. Ron says it best:
I know we always like to say it'll be easier to do it now than it will be to do later. Not likely: I plan to be smarter later than I am now, I plan to have the same tests, and I plan to have an actual need that will direct what I do, not my current fantasy about what's needed.

I think I'll print that out and paste it on my monitor. Things definitely go more smoothly if you just keep it simple and bite off little pieces at a time. Now, to be more religious about it in practice.

Monday, October 23, 2006

New Machine - All Beefy and Everything!

I got a new motherboard and cpu last week and put everything together this past weekend. I didn't feel like getting a ton of new ram, so I went with a socket 939 board. After some research, I ended up getting the Asus A8N5X with an AMD Athlon 64 3800. I have to say it was one of the smoothest installs I've done. I feel comfortable recommending this motherboard who is in the market for something similar. It was nice to have everything "just work".

So far, I've been happy with the AMD 64 - my third AMD. I know the Core2 Duo is the hot kid on the block these days, but I've been finding myself more and more turned off to Intel and Windows lately, especially with DRM (don't get me started on PDF DRM!), so I feel better choosing AMD for my platform. The rest of my system consists of 2GB of PC3200, a GeForce 7800GT and a 250GB WD SATA drive. I have dual 20" LCD's - one is a Dell FP2001 and the other is a ViewSonic VP201b. While I like the picture a little better on ViewSonic, I have this annoying problem where once in a while it won't turn on! I'll boot Windows and have all the icons that were on my extended desktop thrust onto my primary display! Sucks. I may send it in for warranty repair - unless this is the way it's supposed to work! :-p

So, let's see how Call Of Duty 2 performs with this system! (I have to say, CoD2 is one of my favorite games - graphics are amazing, great gameplay, and I love the controls).

Saturday, August 12, 2006

Lookee what I got!

The box is still unopened - just got back from buying it. I'm excited! The demo was great - it's been a while since I've been this excited about a first person shooter.

I was really tempted to buy the special edition DVD because, well, it's on DVD, and because you get two figurines with it. But, I didn't want them badly enough to justify the extra $10. Although, it would have been nice having it on DVD. I hate swapping through ... [opens box to see how many CDs are there] ... three CDs to install a game. Hmm. Actually, that's not as bad as I thought it would be. F.E.A.R. came on five or six CDs! Still, the DVD would be nice.

Saturday, August 05, 2006

Programmers and the Software Product Cycle

Yes, I admit it. I'm a bit of a John Carmack fanboy. So, when there's an article about a presentation he gave, I usually read it. What I admire about him is his ability to quickly analyze a problem, reduce it down to a critical set of parameters, and then come up a set of compromises that allow for a near perfect solution. He's done it so many times. It's inspiring.

Anyway, I was reading this article titled Things I Think John Carmack Said At QuakeCon and was really struck by the truth of this paragraph.

"It used to be a joke at id when someone would say, 'How long would something take?' 'Two weeks.' Because a programmer can get anything done in two weeks," Carmack joked. "Fourteen days, if you stayed up, you could just get anything done in that time. Of course, it doesn't actually work out like that. In any of the projects I've worked on, I've never failed in any of them, but I've never been on time."

It's true! When you're assigned to a programming task you've never done before, you have some idea what needs to be done, but 99.9 percent of the time, there's something that comes up that you didn't anticipate that is a real bitch to work around. The 0.1 percent is for the few times that I write a bunch of code, compile it, and miraculously it just "works". It's a great feeling, but it makes me really nervous at the same time because there's no such thing as "bug free code".

For me, one of the worst things to experience on the job is having a manager come in to your office and say, "I want you to work on [this]. It's extremely critical that it be done on time. How long will it take you to do it?". Ugh! I want to reply, "I don't know. How long will it take for you to train to be able to clean and jerk 500lbs over your head?".

Probaly the best manager I ever had would ask me how long I think it would take and then would triple it for the product planning sheet. It was usually dead on.

Good managers are so hard to find.

Saturday, June 03, 2006

Half-Life - Episode One

That didn't take long! I just finished playing Half-Life 2 : Episode One. While I hate the Steam platform, I'm happy that Valve continues to put out quality game content. Half-Life 2 was one of the best first person shooters I've ever played, so I was looking forward to Episode One. If you haven't played through it and are planning to, don't worry, I don't have any spoilers below.

The quality of the content was excellent - great levels, creative situations - but, like Sin Episodes, was still too short for the $20 they're asking. If it
were closer to $10, I'd buy it without even thinking, but if they keep the current asking price of $20 for the next episode, I think I'll end up waiting until the price comes down.

As I was playing, I happened to notice Alyx's necklace. It's a cube that looks like it could be some kind of artifact. Perhaps given to her by her father? But for what? Protection?















It's funny, as short as Episode One is, it almost had more story elements and plot twists than the entire game of Half-Life 2. As a result, I've found myself thinking about the game more after I finished. I'm sure I'll go through and play it again, paying more attention to the story details.

Now to get support for Half-Life 2 into Game-Tracker... :-)

I'll end with a cool shot of the Citadel as it's preparing to self destruct.



Tuesday, May 16, 2006

Finally! .NET 2.0 Directory Search Improvements

This may seem insignificant to most, but I was thrilled to see that Microsoft added a static method overload to the Directory.GetFiles() method to include a recursive "SearchOption", which specifies whether the search should return files in only the current directory or search through all subdirectories as well.

public static string[] GetFiles (
string path,
string searchPattern,
SearchOption searchOption
)


But, before I got too excited, I found they still don't allow multiple search patterns. You must make a call for each pattern you are searching for. :-(

Still, this improvement saves having to write or reuse that recursive directory search function you've been using forever. It also allows for quick and easy accumulation of files into a collection. For example:
           
ArrayList files = new ArrayList();
try
{
files.AddRange(Directory.GetFiles(@"C:\", "*.jpg",
SearchOption.AllDirectories));
files.AddRange(Directory.GetFiles(@"C:\", "*.bmp",
SearchOption.AllDirectories));
files.AddRange(Directory.GetFiles(@"C:\", "*.tif?",
SearchOption.AllDirectories));
}
catch (UnauthorizedAccessException expectedException)
{
}

As small as this change seems, it's a welcome change!

Terry

Saturday, May 13, 2006

Sin Episodes


Well, I took the plunge and bought Sin Episodes : Emergence off of Steam. Mostly because I loved the first Sin game and was curious to see if this team could do it again. Other than the length and the cost, it wasn't too bad, but it had lost the character and attitude of the first Sin game. :-( If the "episode" were only $10, I would say it's a good buy for the money, but at $20, it's just lacking enough content to make it feel like you got your money's worth. I did enjoy the last series of levels - the way you climb the building and slowly make your way to the top was a lot of fun. I don't know if Levelord made that level or not, but I wouldn't be surprised. In Sin, Levelord continually made top-notch, creative levels doing stuff that nobody else had done before. I played deathmatch for hours on "Behind Zee Bookcase". Great stuff!

Elexis is a little *too* sexy, though. The first game, she was meant to be sexy, but still look like a mean bitch. In this version, she just doesn't look mean. Hard to take her seriously as a villian when she looks like a bimbo sex kitten. (No, she doesn't have some strange skin disease. In the scene this was captured from, she was appearing to Blade as a hologram, so there's some funky video effect)

w00t

w00t! I have a blog! ;-)