After I'd done it twice, I suddenly realised that I was duplicating my efforts and my rule of thumb is, "if you do it more than once, create a class to do it for you."
So here's what I came up with. A LauncherControl, which uses reflection to get a list of windows in the calling assembly and then adds a button for each one to the control.
As I can't upload files, I'll just post the markup and code and you can build one yourself.
- Create a WPF User Control Library. Call it what you like. Mine's called Widgets.
- Delete the UserControl1 and add a new UserControl called LauncherControl.
- Change the root layout into a StackPanel, or whatever takes your fancy and call it controlRoot.
- Tell the controlRoot panel to handle all Click events for all its buttons (that'll save me from having to manually hook up event handlers for every button I create later as I would in WinForms)
- Switch to LauncherControl.xaml.cs and code it up
- And here's that string extension method to prettify the window's name:
static class StringExtensions
{
public static string InjectSpaces(
this string stringToFormat)
{
StringBuilder sb = new StringBuilder();
char[] chars = stringToFormat.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
if (i == 0)
{
sb.Append(chars[i]);
}
else if (char.IsUpper(chars[i]))
{
sb.AppendFormat(" {0}", chars[i]);
}
else
{
sb.Append(chars[i]);
}
}
return sb.ToString();
}
}
So now I can just add a reference to Widgets.dll to every project I want to use the launcher in, and have the startupuri be a window called LauncherWindow, which contains that control.
But then I thought, hang on, there's some more duplication there. Let's take this one step further and have the LauncherWindow contained in the Widgets lib, so I can just set that as the StartupUri and cut out the middleman if I want to. So here goes: - Add a Window to the Widgets library, called LauncherWindow. It should look like this:
StartupUri="pack://application:,,,/Widgets;component/LauncherWindow.xaml"
<UserControl x:Class="Widgets.LauncherControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Name="controlRoot" Button.Click="ButtonClick">
</StackPanel>
</UserControl>
What we need to do in here is to get the UserControl to look at all the types in the assembly that's using the control and find all of the ones that derive from Window. I'm also checking to ensure that if I have a window in the calling assembly called Launcher, it gets ignored (v2.0 - have a DP called StringsToIgnore or something).
Took me a little while to figure out which method of Assembly to call - my first stab was GetCallingAssembly, but that ended up being mscorlib.dll, so GetEntryAssembly gives me the assembly that contains the app's entry point. Obvious really...
Having done that, I'm going to create a new btn with a name of the window class's name, and display some nice(ish) content on the button using a string extension method (InjectSpaces) which is defined further down. (v2.0 - instantiate each Window class and ask it for its Title property). Finally, I'm going to add that button to the controlRoot panel:
public LauncherControl()
{
InitializeComponent();
Assembly assy = Assembly.GetEntryAssembly();
var qry = from t in assy.GetTypes()
where t.BaseType == typeof(Window) &&
!t.Name.Contains("Launcher")
orderby t.Name
select t;
foreach (var item in qry)
{
Button btn = new Button();
btn.Name = item.Name;
btn.Margin = new Thickness(5);
btn.Content = item.Name.InjectSpaces();
controlRoot.Children.Add(btn);
}
}
OK, now we can implement the ButtonClick event handler. What we need to do here is grab the Source of the RoutedEvent (which will be whatever button was clicked to raise the event), find out its name, which you will recall is the same as the name of the Window class, get that Type from the EntryAssembly and use Activator to instantiate it.
Finally, call its ShowDialog() method.
(v2.0 - tweak performance by making the EntryAssembly static)
private void ButtonClick(object sender,
RoutedEventArgs e)
{
Button source = e.Source as Button;
if (source == null)
{
return;
}
Type t = Assembly.GetEntryAssembly().GetTypes()
.First(bType => bType.Name == source.Name);
Window w = Activator.CreateInstance(t) as Window;
w.ShowDialog();
}
<Window x:Class="Widgets.LauncherWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Widgets"
Title="LauncherWindow" Height="300" Width="300">
<StackPanel>
<local:LauncherControl/>
</StackPanel>
</Window>
OK, so we've got the Window hosting the UserControl. I felt this was an important way to do it, rather than re-jigging the UserControl as Window, because consumers get a choice of how to use it - they might want a switchboard form with other stuff on it, or just the basic switchboard. Choice is all important!
Finally, I had to work out how to get a client App to use the LauncherWindow for it's StartupUri. I won't share how many attempts I made before finally succumbing to rtfming; suffice to say that I checked out MSDN and found the solution - pack uris! Following is the setting for StartupUri if you've called your widgets project Widgets and your switchboard window LauncherWindow. I'm sure you're clever enough to figure out the differences if you didn't...
Not bad eh? There may be one or two problems with this code which I may or may not come back and correct. But that should do it for now...