Monday, April 23, 2007
More ClickOnce Trials
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)
- 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
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/
"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.
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
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()"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.
{
GlobalSettings global = AppSettings.Instance.Global;
if (global.MainWindowState != FormWindowState.Minimized)
{
createMainForm();
}
}
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
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
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 ...
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!
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).