Monday, April 23, 2007

More ClickOnce Trials

Wow, I can't believe that I haven't posted in about four months! In that time, I've been able to use ClickOnce for deploying and updating my program to a few friends for testing. It was a bit frustrating at first working out some of the kinks and now I have a pattern down so I can whip out a new version in a few minutes. I ended up modifying the procedure a little bit from the article I posted in my last post, which I think works a little better for me.

Now, I'd like to automate it in an MSBuild target and combine it with some Perforce scripting to update the publish.htm file with changes since the last build. I found a blog from someone who looks like worked through this very problem. I'll try it and see how it works.


I didn't want the delay at startup of the ClickOnce option to check before startup, so I went with the asynchronous "after startup" option. But, then you're left with having to handle the event, handle letting the user know that there's an update and tell them to restart the app. Only then, does the update happen. It was really clunky. So, I ended up making more use of the ClickOnce API to handle downloading the update and restarting the app myself. In the screenshot above, the little globe in the lower right corner checks for updates when you double-click on it. If there's an update, you're presented with a dialog that lets you download the update. Progress is shown in a progress bar in the status area of the main window, just left of the update icon (globe). When it's done, you can restart the app, or continue running and restart later. The API is pretty well done and updates are going very smoothly now. It was a bit more work, but it was worth it in the end to have an automated update mechanism built right into the app with a small amount of work.

I've also added text to speech capability in the built-in chat panel and some custom drawn server lists. I'm pretty new to GDI and custom drawn controls, and I can tell I need a lot of optimization to get it to the point where it can handle thousands of servers.

More later.

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).