.NET Code Injection
July 2, 2008
In this post I’ll show how you can inject your own .NET code into a process that is already running the .NET framework.
I needed to do this when developing for Windows Media Center because there is no official documented way to determine what page is currently being displayed in Media Center.
I wanted to know when the user is showing the Media Center “now playing” page.
Media Center addins run in a completely separate process to the main Media Center process, which is ehshell.exe. Calls you make in your addin are remoted across to the Media Center ehshell.exe process.
The solution works using:
- an “Injecter” C# class which uses CreateRemoteThread to load a Bootstrap DLL into the ehshell.exe process
- The “Bootstrap” C++ DLL which is injected into the ehshell.exe process uses the CLR Hosting API to attach to the default .NET domain, and then load an instance of the “Injectee” C# class
- The “Injectee” C# class is loaded by the “Bootstrap” DLL and runs within the ehshell.exe process. It uses reflection to subscribe to an event within the ehshell.exe process and then sends an interprocess message when the event fires
The “Injecter”
Using Reflector, I looked around at the ehshell.exe assembly, and related assemblies, and eventually discovered that there is a PageChanged event fired by the ServiceBus.UIFramework.PageBasedUCPService class:
What is more, there is a static PageBasedUCPService.DefaultUCP method to return a PageBasedUCPService instance.
There are of course a couple of problems. The first problem being that these classes are internal to the ehshell assembly, the second issue being that these are all running in a totally separate process to my addin.
I searched a little, and came across this example showing how you could use the CreateRemoteThread from .NET to inject your own native code into a remote process.
Essentially it opens a handle to the remote process, finds the LoadLibrary system call’s address in the remote process, allocates a chunk of memory in the process, and fills the memory with a LoadLibrary call to load a DLL that you specify, and then creates the thread in the remote process, passing the address of the chunk of memory as the starting point for the thread.
I recommend taking a look at the CRT call in the example. I’ll not be duplicating the code here.
To use the CRT call, I first find Media Center:
public static Process FindMediaCenterProcess() { // Could be more than one if an extender is running too foreach (Process process in Process.GetProcessesByName(“ehshell”)) { if (process.SessionId == Process.GetCurrentProcess().SessionId) { return process; } } return null; }
I can then call the CRT method:
Process mediaCenterProcess = FindMediaCenterProcess(); IntPtr hwnd; string error; CRT(mediaCenterProcess, dllPath, out error, out hwnd);
The dllPath is the full path to my C++ DLL, in my case this was
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) +
@”\Atadore\MceFM\InjectBootstrap.dll”
The “Boostrap DLL”
The InjectBootstrap.dll is a pretty simple C++ library project. The DllMain calls a bootstrap function when the process is loaded:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Bootstrap();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
#include “stdafx.h” #include <stdio.h> #include “objbase.h” #include “MSCorEE.h” #import “C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.tlb” raw_interfaces_only using namespace mscorlib; void Bootstrap() { OutputDebugString(L“MceFM Bootstrap Started”); CoInitializeEx(0, COINIT_MULTITHREADED ); ICorRuntimeHost* pICorRuntimeHost = 0; HRESULT st = CoCreateInstance(CLSID_CorRuntimeHost, 0, CLSCTX_ALL, IID_ICorRuntimeHost, (void**)&pICorRuntimeHost); if(!pICorRuntimeHost) return; // Clean up and log errror … HDOMAINENUM hEnum = NULL; pICorRuntimeHost->EnumDomains(&hEnum); if(!hEnum) return; // Clean up and log errror … IUnknown* pUunk = 0; st = pICorRuntimeHost->NextDomain(hEnum, &pUunk); if(!pUunk) return; // Clean up and log errror … _AppDomain * pCurDomain = NULL; st = pUunk->QueryInterface(__uuidof(_AppDomain), (VOID**)&pCurDomain); if(!pCurDomain) return; // Clean up and log errror … _bstr_t assemblyName = “Last, Version=1.1.0.0, Culture=neutral, PublicKeyToken=792d614cdf38e9ce”; _bstr_t typeName = “MceFM.Last.Inject.Injectee”; _ObjectHandle* pObjectHandle = 0; pCurDomain->CreateInstance(assemblyName, typeName, &pObjectHandle); }
The “Injectee”
Finally we come to the Injectee class, which is loaded by the DLL, and runs inside the remote (ehshell.exe) process. It is in a strongly named assembly which is stored in the GAC, so that the bootstrap call above can find it (note the strong name in the assembly name bstr).
I use a static constructor (which runs as soon as the class is loaded). In this static constructor I subscribe to the event:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Reflection; using System.Reflection.Emit; using System.Threading; namespace MceFM.Last.Inject { public class Injectee { static Injectee() { try { Assembly ehshell = Assembly.Load(“ehshell”); // Get the PageBasedUCPService type Type pageBasedUCPServiceType = ehshell.GetType(“ServiceBus.UIFramework.PageBasedUCPService”); // Call static DefaultUCP method to get a PageBasedUCPService instance const BindingFlags bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty; object[] args = new object[0]; object defaultUCP = pageBasedUCPServiceType.InvokeMember(“DefaultUCP”, bindingFlags, null, null, args); // Get a reference to the PageChanged event EventInfo pageChangedEventInfo = pageBasedUCPServiceType.GetEvent(“PageChanged”); Type pageChangedDelegate = pageChangedEventInfo.EventHandlerType; // Get the MethodInfo for the method to be called when the event fires MethodInfo newPageHandlerMethodInfo = typeof(Injectee).GetMethod(“NewPageHandler”); // Build an array of types used for new method we shall dynamically emit Type pageType = ehshell.GetType(“ServiceBus.UIFramework.Page”); Type[] pageChangedDelegateParameters = new[] { pageType }; // Need a dynamic method because we can’t create a method that has // the Page type as a parameter DynamicMethod dynamicHandler = new DynamicMethod(“”, null, pageChangedDelegateParameters, typeof(Injectee)); ILGenerator ilgen = dynamicHandler.GetILGenerator(); ilgen.Emit(OpCodes.Nop); ilgen.Emit(OpCodes.Ldarg_0); ilgen.Emit(OpCodes.Call, newPageHandlerMethodInfo); ilgen.Emit(OpCodes.Nop); ilgen.Emit(OpCodes.Ret); if(pageChangedDelegate != null) { // Subscribe to the event Delegate dEmitted = dynamicHandler.CreateDelegate(pageChangedDelegate); pageChangedEventInfo.GetAddMethod().Invoke(defaultUCP, new object[] {dEmitted}); } } catch(Exception ex) { Trace.TraceError(“Unexpected error in Injectee initializer: {0}”, ex); Trace.TraceError(ex.StackTrace); } } // Queue of new pages that have been navigated to. Decouples event handler // from re-despatching of the event to the addin process that is told // of the new event. private static readonly Queue<string> newPages = new Queue<string>(); // Indicates whether background thread that despatches events is running private static bool notifierActive; // Called when Media Center navigates to a new page public static void NewPageHandler(object page) { Trace.TraceInformation(“New Page: “ + page); lock(newPages) { if(!notifierActive) { Thread thread = new Thread(NewPageNotifier) {IsBackground = true}; thread.Start(); notifierActive = true; } newPages.Enqueue(page.GetType().FullName); Monitor.Pulse(newPages); } } // Thread that despatches events to addin process telling it that // a new page has been navigated to private static void NewPageNotifier() { while(true) { string page; lock(newPages) { while(newPages.Count == 0) { Monitor.Wait(newPages); } page = newPages.Dequeue(); } try { // Use whatever interprocess notification mechanism you wish. // WCF could be good. Here I use a simple web call. My // addin has a web server in it to receive these calls. WebClient webClient = new WebClient(); webClient.QueryString[Server.PAGE_QUERY_STRING] = page; webClient.DownloadString(Util.LocalBaseUrl + Server.MCE_PAGE_CHANGED_ACTION); } catch (Exception ex) { Trace.TraceError(“Error while sending new page notification: {0}”, ex); } } #pragma warning disable FunctionNeverReturns } #pragma warning restore FunctionNeverReturns } }
The Result
In my web server running within my addin (which is notified by the “NewPageNotifier” method above), I keep track of which is the current page, and log information when the page changes:
Server notified of page change MediaCenter.Audio.AudioNowPlayingPage Server notified of page change MediaCenter.Audio.AudioNowPlayingTracklistPage
All this means that if you are a Last.fm user and want to tell Last.fm that you love a track that is currently playing then you can press the right-button on your remote control on the “now playing” page using my MceFM addin.
I use low level system hooks to know when you press the right button, and the above technique to know that you are on the “now playing” page:
Word if warning. If/When Media Center changes its internal structure this technique will completely fall over. Use sparingly, entirely at your own risk, to do the seemingly impossible.
Windows Media Center (MCE) lets you create addins to provide new functionality. There are two kinds of addins — normal addins, and background addins. Background addins run continuously as soon as MCE starts up, and are limited in the user-interaction they provide.
One thing they are allowed to do is to pop up a dialog message to the user using the AddInHost.Current.MediaCenterEnvironment.Dialog(…) method call.
I was doing this in my background addin, but I was getting an error from MCE saying that I was not allowed to make that call.
Addins run in separate processes to the main MCE process, which is ehshell.exe, and the Dialog call is remoted into the ehshell.exe process.
Using Reflector I could see that the call was eventually failing in MediaCenter.Extensibility.ExtensibilityAutomation.EnforceApplicationPermission(…) which was calling through to Microsoft.MediaCenter.Hosting.Infrastructure.RegisteredApps.IsBackgroundEntryPoint which looks like this:
public static bool IsBackgroundEntryPoint(ExtensibilityEntryPointInfo epi)
{
return (0 == string.Compare(epi.Category, “Background”, true,
CultureInfo.InvariantCulture));
}
Somehow I needed a way to use the debugger to see what the epi.Category value was when the call was failing.
The standard Visual Studio debugger just drops you through to x86 assembly code if it has no debugging symbols. I needed a debugger which shows .NET IL instead.
This is where mdbg comes in. Mdbg is a command line debugger created by Mike Stall.
Once you have downloaded it and started running it, then you can use the ‘a‘ command to list available processes, and then ‘a <pid>‘ to attach to a specific process.
Once attached, you can set a breakpoint using ‘br <module name>!<type>.<method name>“, and then ‘go‘ to resume:
Once the breakpoint is hit, you can use the ‘print‘ command to display a parameter value:
There is also a GUI which can be invoked using ‘load gui’:
Attending PDC 2008
June 7, 2008
Last year Microsoft announced the 2007 Professional Developers Conference (PDC) and I immediately booked the flights and hotel. They then canceled the PDC. Nevertheless I flew out to LA and had a great time spending a week immersed in technology, doing my very own “Personal” PDC, with no interruptions, spending all the hours I wanted. It took me right back to the days before I was married, before I had kids, and I guess before I had a life.
Microsoft have just announced the 2008 PDC in LA, and I’ve signed up for it. I’m pretty sure this one won’t get canceled.
If you are attending and feel like a chat about anything, including any of the products I’ve created such as MceFM, the ListSearch Extender, PromptSQL or J-Integra or anything else please email me at damian at atadore.com.
MceFM 1.0 Beta 6 released
June 6, 2008
I’m finally feeling as though I am getting very close to the first non-beta release of MceFM.
I decided to remove MceFM’s Queue page (which was by all accounts rather ugly anyway) and just to use the standard Windows Media Center music related pages (Now Playing, and View Queue).
MSAS Fun
I needed to detect Media Center events in order to know when a song changes, etc.
The standard way to do this is using the Media State Aggregation Service (MSAS), and I got this working. However it is rather flaky, and in the end I threw away the MSAS code I’d written, and took a different approach. The code is here if anyone wants it.
I’m embedding an instance of the Windows Media Player (WMP) ActiveX Control in a hidden window attached to the MceFM background application. The WMP control runs in “remote” mode, so that it reflects what is happening with the WMP instance used by Media Center. Using this control I can detect when songs start playing.
Detecting Remote Control button pushes
I also went through a weird and wonderful journey with regards to allowing users mark songs as Loved or Banned in Last.fm. Since I no longer have my own Queue page, I somehow needed to let the user press unused keys on the remote control when the Media Center “Now Playing” page was displayed. Two things were needed — how to detect when the key is pressed, and how to know when the Now Playing page is being displayed.
I initially decided to use the colored buttons that appear on my remote control (Red, Green, Yellow, Blue) which are used for teletext. These buttons can’t be intercepted using the normal techniques (low level system hooks), but can be detected using RegisterRawInputDevices and looking for RAWINPUT Windows Messages. I got this working fine on my computer (RAWINPUT 91=Red, 92=Green, 93=Yellow, 94=Blue).
My idea was to let people press the Red button to Ban a song, and the Green button to Love a song. I got this all working great. Then I hit two issues.
The first issue is that the RegisterRawInputDevices mechanism didn’t work at all for extenders.
The second issue is that only European Media Center remote controls appear to have the colored buttons. D’oh.
I had to throw the whole approach overboard and start again from scratch.
I ended up letting you press the Left or Right arrow buttons on the remote control, which can be detected using low level system hooks, and do work on Extenders. Whew.
When Resharper isn’t so sharp
June 6, 2008
One of the things I wanted to add to MceFM was to let users tell Last.fm to Love/Ban songs.
I was using the RegisterRawInputDevices Windows call to listen for events generated by the remote control. This takes a RAWINPUTDEVICE structure as a parameter:
[StructLayout(LayoutKind.Sequential)] internal struct RAWINPUTHEADER { [MarshalAs(UnmanagedType.U4)] public int dwType; [MarshalAs(UnmanagedType.U4)] public int dwSize; public IntPtr hDevice; [MarshalAs(UnmanagedType.U4)] public int wParam; }
My code was working fine, and then suddenly stopped working. I lost at least an hour tracking back what I’d changed recently, and then finally realized that I’d run Resharper’s Reformat Code tool:
Notice that last item? This is what it did to the structure:
[StructLayout(LayoutKind.Sequential)] internal struct RAWINPUTHEADER { [MarshalAs(UnmanagedType.U4)] public int dwSize; [MarshalAs(UnmanagedType.U4)] public int dwType; public IntPtr hDevice; [MarshalAs(UnmanagedType.U4)] public int wParam; }
The RegisterRawInputDevices call was falling over because the structure was totally out of whack. I’m still a big fan of Resharper — and this is marked as fixed in the next release.
Creating MSAS sinks in C# without using MediaState
May 20, 2008
Microsoft’s Window Media Center has a mechanism to notify you of events inside Media Center, called the Media State Aggregation Service (MSAS).
The Media Center SDK includes a .NET assembly called MediaState, which you can use to hook up C# to MSAS, however it is possible to build C# classes that hook directly to MSAS without using the MediaState assembly.
To do so, create a C# Class Library project, and add a COM reference (Project|AddReference|Browse) to c:\windows\ehome\ehmsas.exe.
Next create a couple of classes, one called Sink and the other called Session.
Make the Sink class implement the MediaStatusSink interface from ehMSASLib imported COM library:
using ehMSASLib; using System.Diagnostics; using System.Runtime.InteropServices; namespace TestMSAS { [ComVisible(true)] [GuidAttribute("360d8298-97d9-4058-9052-b91efe76d3ae")] public class Sink : MediaStatusSink { public Sink() { string logFile = string.Format(“{0}MsasSink.log”, System.IO.Path.GetTempPath()); Trace.Listeners.Add(new TextWriterTraceListener(logFile)); Trace.AutoFlush = true; Trace.TraceInformation(“Sink started”); } public void Initialize() { Trace.TraceInformation((“Sink.Initialize called”)); } public MediaStatusSession CreateSession() { Trace.TraceInformation((“Sink.CreateSession called”)); return new Session(); } } }
I’ve made the CreateSession method return a new instance of the Session class, and also made the Sink class public and added the ComVisible(true) attribute to make it visible from COM. You should generate and use your own Guid using Tools|Create GUID.
Make the Session class implement the MediaStatusSession interface:
using System; using System.Diagnostics; using System.IO; using System.Net; using ehMSASLib; namespace TestMSAS { class Session : MediaStatusSession { void IMediaStatusSession.MediaStatusChange(Array tags, Array properties) { Trace.TraceInformation(“Session.MediaStatusChange called”); for (int i = 0; i < tags.Length; i++ ) { MEDIASTATUSPROPERTYTAG tag = (MEDIASTATUSPROPERTYTAG) tags.GetValue(i); object value = properties.GetValue(i); Trace.TraceInformation(“Tag {0}={1}”, tag, value); } } public void Close() { Trace.TraceInformation((“Session.Close called”)); } } }
We are going to put the C# assembly in the GAC, so make sure it is strongly named (Project|Properties|Signing|Sign the assembly):
Once you have built your project, register the resulting DLL for COM access using the regasm command, and install both the DLL, and the Interop.ehMSASLib.dll interop assembly in the GAC using the gacutil command (I missed registering the interop assembly and spent a couple of hours banging my head against the wall):
Finally, fire up the registry editor, find the HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{…} key for your Sink class (I searched for it to find it), and add a new KEY under the “Implemented Categories” key, with the name “{FCB0C2A3-9747-4C95-9D02-820AFEDEF13F}” (no quotes). This tells MSAS that your component acts as an MSAS Sink:
To test, I generally kill the ehmsas process from the task manager, and the fire up Media Center, which restarts the ehmas process.
You can also debug directly by setting the debug executable to be c:\windows\ehome\ehmsas.exe and setting the executable paramerers to “-embedded”
Bulk fetching images for dvr-ms files
April 28, 2008
When using Windows Media Center, there is a hidden option to make it display a “my dvds” menu item. As well as DVDs, it also shows all your recorded films.
By default there is no thumbnail image displayed for these recorded films (which end in the “dvr-ms” extension):
You can make a thumbnail be displayed by downloading the appropriate cover art jpeg image, and name it the same as the film’s file, but with the “.jpg” extension, instead of “.dvr-ms”.
I have well over a hundred such films, and it was going to take all night to download each film’s cover from amazon/google, so I’ve created a program which does it in bulk. You chose a directory containing your dvr-ms files, and for each film that doesn’t already have an image, it looks up the film name in Google Images, and lets you choose the appropriate image:
After you click on the image you want, it creates the JPEG file for the film, and carries on to the next film. It uses the metadata in the DVR-MS files to determine if the file is a film, and only prompts you for films.
You can download the program here. Download it to your computer before running it. It is written in C# and is free for you to use — use it entirely at your own risk– feel free to report bugs here though.
MceFM released: Last.fm addin for Windows Media Center
March 17, 2008
Over the last few months I’ve been working on an addin to Windows Media Center which lets you listen to Last.fm based off of the artists in your music collection (you can also enter arbitrary artists names etc.).
I wanted it to work when the user wasn’t running the MceFM application within Media Center, which meant creating a background application and a normal application. That way you can listen to songs and watch your photo collection (for example).
Also, the Last.fm music streams don’t include the tags normally included in mp3 files, which let you see the track name, artist, album name, etc. So what I did was to dynamically add these tags to the stream, which means that Media Center will shown all that information correctly.
All in all, a lot of fun to develop. Now I just need to see if anyone cares :-)
More at http://www.mcefm.com/
Referencing external assemblies using MCML
March 9, 2008
MCML is Windows Media Center Markup Language. You use it to define the GUI for your Windows Media Center applications.
I’ve been banging my head against the wall trying to get a reference to a .NET assembly to work in MCML. The example in the documentation uses the syntax:
xmlns:cor=“assembly://MSCorLib/System”
Where MSCorLib is the name of the assembly in the Global Assembly Cache.
This worked fine for me where the assembly contained the current Media Center Application:
xmlns:ext=“assembly://MceAppAssembly/SomeNamespace”
However when I wanted to reference a different external assembly (that was also in the GAC) I was getting an error loading the MCML page, indicating that the assembly could not be loaded.
In the end I found out that I needed to use a strong assembly name:
xmlns:ext=“assembly://ExternalAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456789abcdef/SomeNamespace”
I guess this is pretty obvious, but I’m also guessing that I won’t be the only one to waste an hour or so on this. The documentation doesn’t give an example like this (just the weak named reference), although it does give strong named examples for resource references.
Handling background threads using NUnit and Rhino Mocks
March 6, 2008
I’ve just started using NUnit and Rhino Mocks, so this is not necessarily the best way to do things, but it works for me.
I have a class which has a method which kicks off a background thread. This thread has an infinite loop — for each loop it accepts a request and services the request.
I needed to test an instance of the class, for one specific incoming request. There are two issues — how to know when the object’s background thread has finished servicing the request, and how to stop the background thread from looping back and accepting another request.
This is what I ended up doing:
[TestFixture] public class ServerTest { [Test] public void SomeTest() { bool done = false; MockRepository mocks = new MockRepository(); using(mocks.Record()) { // … lots of ‘Expects’ // This is the final call which gets made in the background thread // to complete the handling of the request Expect.Call(someObject.LastMethod).Do((Expect.Action)delegate() { done = true; System.Threading.Thread.CurrentThread.Abort(); }); } using(mocks.Playback()) { Server server = new Server(); server.Start(); int count = 0; while(!done && count < 10) { System.Threading.Thread.Sleep(1000); count++; } } } }
The key point is that in the test’s action associated with the last method that gets invoked as part of your background thread’s processing loop, you need to set a flag so that the test knows that the last method has executed, and also abort the current thread so that the background thread doesn’t loop back, and consume another request.
Configuring HTTP Namespace reservations on Vista using WIX
February 21, 2008
WIX is an XML based installer generator, from Microsoft, now hosted in Sourceforge.
As part of an installation for a Vista based product I am creating, I needed to register an http namespace. This is required in order to listen for incoming connections. You can do this from the command line as described here: http://msdn2.microsoft.com/en-us/library/ms733768.aspx
I spent a day or so researching, and coding a Custom Action, which worked. As I was in the final stages of integrating it into my WIX wxs file, I discovered a much simpler way of doing it, based on the way Microsoft Media Center applications are registered in the template code created in Visual Studio 2008 by the Windows Media Center SDK.
The first thing I did was to define a property which points to the fully qualified Netsh.exe tool:
<!– This property uses an AppSearch to attempt to –> <!– locate the file %windir\Netsh.exe –> <Property Id=“NETSH” Secure=“yes“> <DirectorySearch Id=“Netshdir” Path=“[SystemFolder]“> <FileSearch Id=“Netshexe” Name=“Netsh.exe” /> </DirectorySearch> </Property>
Then I defined a couple of properties that describe the Netsh commands to register and unregister the HTTP namespace. You’d need to replace the port, ‘Test’ and possibly specify something else instead of ‘Users’:
<Property Id=“NamespaceAddCmd” Value=“http add urlacl url=http://+:123/Test/ user=Users“/> <Property Id=“NamespaceDelCmd” Value=“http del urlacl url=http://+:123/Test/“/>
Next I define the custom actions used when installing and uninstalling. These are based on those used for registering MCE apps. The ‘…_Cmd’ actions set properties which are then used by the actions they target. This is because deferred actions can not access normal properties, only the CustomActionData property. This is set by the ‘…_Cmd’ actions when they have a Property which names the custom action whose CustomActionData they target:
<CustomAction Id=“CA_RegisterNamespace_Unregister_Install_Cmd” Property=“CA_RegisterNamespace_Unregister_Install” Value=“"[NETSH]" [NamespaceDelCmd]“/> <CustomAction Id=“CA_RegisterNamespace_Unregister_Uninstall_Cmd” Property=“CA_RegisterNamespace_Unregister_Uninstall” Value=“"[NETSH]" [NamespaceDelCmd]“/> <CustomAction Id=“CA_RegisterNamespace_Register_Cmd” Property=“CA_RegisterNamespace_Register” Value=“"[NETSH]" [NamespaceAddCmd]“/> <CustomAction Id=“CA_RegisterNamespace_Rollback_Cmd” Property=“CA_RegisterNamespace_Rollback” Value=“"[NETSH]" [NamespaceDelCmd]“/> <CustomAction Id=“CA_RegisterNamespace_Unregister_Install” BinaryKey=“WixCA” DllEntry=“CAQuietExec” Execute=“deferred” Return=“ignore” Impersonate=“no“/> <CustomAction Id=“CA_RegisterNamespace_Unregister_Uninstall” BinaryKey=“WixCA” DllEntry=“CAQuietExec” Execute=“deferred” Return=“ignore” Impersonate=“no“/> <CustomAction Id=“CA_RegisterNamespace_Register” BinaryKey=“WixCA” DllEntry=“CAQuietExec” Execute=“deferred” Return=“check” Impersonate=“no“/> <CustomAction Id=“CA_RegisterNamespace_Rollback” BinaryKey=“WixCA” DllEntry=“CAQuietExec” Execute=“rollback” Return=“ignore” Impersonate=“no“/>
Finally, these are the custom actions used in the InstallExecuteSequence:
<Custom Action=“CA_RegisterNamespace_Unregister_Install_Cmd”
After=“CostFinalize“>
<![CDATA[NOT REMOVE]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Unregister_Uninstall_Cmd”
After=“CA_RegisterNamespace_Unregister_Install_Cmd“>
<![CDATA[REMOVE AND ($Registration.xml = 2)]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Register_Cmd”
After=“CA_RegisterNamespace_Unregister_Uninstall_Cmd“>
<![CDATA[NOT REMOVE]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Rollback_Cmd”
After=“CA_RegisterNamespace_Register_Cmd“>
<![CDATA[NOT REMOVE]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Unregister_Uninstall”
Before=“RemoveFiles“>
<![CDATA[REMOVE AND ($Registration.xml = 2)]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Rollback”
After=“InstallFiles“>
<![CDATA[NOT REMOVE]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Unregister_Install”
After=“CA_RegisterNamespace_Rollback“>
<![CDATA[NOT REMOVE]]>
</Custom>
<Custom Action=“CA_RegisterNamespace_Register”
After=“CA_RegisterNamespace_Unregister_Install“>
<![CDATA[NOT REMOVE]]>
</Custom>
Just in case it is of use to anyone, here is the custom action I coded up, although it is not needed with the above code:
#include <windows.h> #include <msi.h> #include <msiquery.h> #include <Http.h> #include <Sddl.h> #pragma comment(linker, “/EXPORT:ReserveNamespace=_ReserveNamespace@4″) #pragma comment(linker, “/EXPORT:ReleaseNamespace=_ReleaseNamespace@4″) #define BUF_LEN 256 #define LOG true UINT ReserveOrReleaseNamespace (MSIHANDLE hInstall, bool reserve) { HWND parentWindow = GetForegroundWindow(); if(LOG) MessageBox(parentWindow, L“Called”, L“ReserveOrReleaseNamespace”, MB_OK); bool scheduled = MsiGetMode(hInstall, MSIRUNMODE_SCHEDULED); bool rollback = MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK); bool commit = MsiGetMode(hInstall, MSIRUNMODE_COMMIT); if(LOG) MessageBox(parentWindow, scheduled ? L“True” : L“False”, L“ReserveOrReleaseNamespace is MSIRUNMODE_SCHEDULED”, MB_OK); if(LOG) MessageBox(parentWindow, rollback ? L“True” : L“False”, L“ReserveOrReleaseNamespace is MSIRUNMODE_ROLLBACK”, MB_OK); if(LOG) MessageBox(parentWindow, commit ? L“True” : L“False”, L“ReserveOrReleaseNamespace is MSIRUNMODE_COMMIT”, MB_OK); // Initialize HTTP HTTPAPI_VERSION httpApiVersion = HTTPAPI_VERSION_1; HRESULT error = HttpInitialize(httpApiVersion, HTTP_INITIALIZE_CONFIG, 0); if(error != S_OK) return error; // Get the custom action data // (only thing we can pass to deferred custom actions) wchar_t customActionData[BUF_LEN] = L“”; DWORD customActionDataLen = BUF_LEN; MsiGetProperty (hInstall, L“CustomActionData”, customActionData, &customActionDataLen); if(LOG) MessageBox(parentWindow, customActionData, L“ReserveOrReleaseNamespace CustomActionData is”, MB_OK); wchar_t* semicolon = wcschr(customActionData, ‘;’); if(!semicolon) { MessageBox(parentWindow, L“MceFM Namespace Reservation failed because the CustomActionData was incorrect”, L“MceFM Namespace Reservation”, MB_OK); return E_FAIL; } // Extract the user and URL wchar_t* user = customActionData; wchar_t* url = semicolon + 1; *semicolon = ”; if(LOG) MessageBox(parentWindow, url, L“ReserveOrReleaseNamespace NR_URL is”, MB_OK); if(LOG) MessageBox(parentWindow, user, L“ReserveOrReleaseNamespace NR_USER is”, MB_OK); // Convert the user to a SID PSID pSID; DWORD sidSize = SECURITY_MAX_SID_SIZE; pSID = LocalAlloc(LMEM_FIXED, sidSize); WCHAR domainName[BUF_LEN]; DWORD domainNameLen = BUF_LEN; SID_NAME_USE sidNameUse; BOOL worked = LookupAccountName(NULL, user, pSID, &sidSize, domainName, &domainNameLen, &sidNameUse); if(!worked) { error = GetLastError(); if(LOG) MessageBox(parentWindow, L“LookupAccountName failed”, L“ReserveOrReleaseNamespace”, MB_OK); return error; } // Convert the SID to a string LPTSTR stringSid; worked = ConvertSidToStringSid(pSID, &stringSid); if(!worked) { error = GetLastError(); if(LOG) MessageBox(parentWindow, L“ConvertSidToStringSid failed”, L“ReserveOrReleaseNamespace”, MB_OK); LocalFree(pSID); return error; } // Build a security descriptor in the required format WCHAR securityDescriptor[BUF_LEN]; wsprintf(securityDescriptor, L“D:(A;;GX;;;%s)”, stringSid); // Set up the config info (url and security descriptor) HTTP_SERVICE_CONFIG_URLACL_SET configInfo; configInfo.KeyDesc.pUrlPrefix = url; configInfo.ParamDesc.pStringSecurityDescriptor = securityDescriptor; if(reserve) { // Add the namespace reservation error = HttpSetServiceConfiguration(0, HttpServiceConfigUrlAclInfo, &configInfo, sizeof(HTTP_SERVICE_CONFIG_URLACL_SET), NULL); if(error != S_OK) { if(LOG) MessageBox(parentWindow, L“HttpSetServiceConfiguration failed”, L“ReserveOrReleaseNamespace”, MB_OK); } } else { // Remove the namespace reservation error = HttpDeleteServiceConfiguration(0, HttpServiceConfigUrlAclInfo, &configInfo, sizeof(HTTP_SERVICE_CONFIG_URLACL_SET), NULL); if(error != S_OK) { if(LOG) MessageBox(parentWindow, L“HttpDeleteServiceConfiguration failed”, L“ReserveOrReleaseNamespace”, MB_OK); } } LocalFree(pSID); LocalFree(stringSid); return error; } extern “C” UINT __stdcall ReserveNamespace (MSIHANDLE hInstall) { return ReserveOrReleaseNamespace(hInstall, true); } extern “C” UINT __stdcall ReleaseNamespace (MSIHANDLE hInstall) { return ReserveOrReleaseNamespace(hInstall, false); }
Dealing with "Sys.InvalidOperationException: Handler was not added through the Sys.UI.DomEvent.addHandler method."
August 13, 2007
In the ListSearch extender I recently had a bug where you got an exception when leaving a page that had a ListBox on it, where the ListBox was the target of both a CascadingDropDown extender and a ListSearch extender.
The reason was pretty dumb– I was calling $clearHandlers(listBox) in the dispose method on the ListSearch Extender, and this was removing the handlers that the CascadingDropDown had set up. So when the CascadingDropDown’s dispose method tried to clean up its event subscriptions, the ASP.NET AJAX runtime complained that it was not subscribed.
The solution was replace calls to $addHandlers and $clearHandlers with individual calls to $addHandler and $removeHandler for each individual event in the ListSearch’s initialize and dispose methods.
Unfortunately after implementing this solution I still got the same error … after a lot of head-scratching, it turned out to be a simple typo. I was calling $addHandler with ‘keydown’ as the event name, and calling $removeHandler with ‘keyDown’ as the event name.
A couple of things for you to check if you get the same error.
My entry for Business Of Software 2007 speaker slot
July 7, 2007
Neil Davidson of Red-Gate Software is running what looks to be an excellent conference on the Business Of Software in San Jose on 29th-30th October.
They have a competition for three speaking slots, and I’ve submitted a 3 minute video entry — take a look and vote for the ones you like by clicking on the stars under the videos. Pay close attention to the one set in the garden :-)
The pain of software product pricing
June 26, 2007
I used to think that I was pretty good at pricing software products I created. I was forced to revise my high opinion of myself when a company bought the IP to a product I’d been selling and increased the price. By a factor of eight.
My first (good) experience in pricing software products came in the late nineties when I released a Java-COM bridging product called J-Integra. I figured I could sell it at US$75 a license, so I initially priced it at US$375 for a five client license pack.
Not long after first releasing it I went to visit Lehman Brothers in Manhattan, at the very bottom of the island. When they found out that the entire deployment, world-wide, of a solution based on J-Integra was going to cost them less than US$1K, they laughed. I cringed.
Instead of taking a taxi back to the mid-town area where I was staying, I walked. And I thought. By the time I got back I’d decided to change the licensing model. I kept the existing US$375 for 5 client licenses, but I added a server license at US$3000. I’d “segmented” my market — depending on how J-Integra was being used, the client or the server license would apply.
No one complained about the price increase, although plenty of people complained about the price in general over the years. I came to realize that this was good. I realized that you want to have enough people complaining about the price so that you know it is not too low, and enough sales so that you know it is not too high.
So how did I end up charging eight times less than the market would bear, when I sold my next product, an SQL Intellisense tool called PromptSQL, for US$25 a copy?
I have plenty of excuses: PromptSQL was a “hobby” product that I created it on the train going to and from my day job as a consultant. One of my friends thought that US$25 a copy was ridiculously high.
The reality is that the US$25 price was OK as an initial “best guess” as to what was appropriate. The mistake I made was in not changing the pricing once it was clear that it was too low. The signs were there — indeed one of the reviews said At just $25 for a single copy, this utility provides value which far exceeds its rather modest cost. That is a pretty clear hint!
Of course it isn’t as clear cut as that. Red-Gate (the company that acquired PromptSQL from me) actually released the version that I’d been selling, for free, while they wrote a new version from scratch. I tell myself that Red-Gate has serious credibility in the market, and they invested a lot in creating a slick new version, and that is why they can charge so much more that I did. And its partly true. But I am sure I could at least have charged US$75 a copy. Ouch.
Dynamically creating ASP.NET AJAX controls
June 26, 2007
In my previous post I showed how you can cache large HTML elements, such as SELECTs in the web browser’s document cache. What if you want to dynamically create and attach ASP.NET AJAX Control Extenders to the dynamically created SELECT?
I’ve already shown how you can call extenders from JavaScript, but if you want to create and delete them from scratch, you can use functions such as these:
function AddListSearchTo(list) { list.listSearch = $create(AjaxControlToolkit.ListSearchBehavior, { ‘promptCssClass’ : ‘listSearchPrompt’ }, {}, {}, list); } function RemoveListSearchFrom(list) { if(list.listSearch ) { list.listSearch .dispose(); list.listSearch = null; } }
I use an expando on the target SELECT to point to the ListSearch Extender that I’ve dynamically created, which means that I can easily get hold of it if I want to remove it.
As you can see you can easily pass initial properties too. The $create function is a shortcut to Sys.Component.create
I use these functions when dynamically fetching HTML SELECTs from the server, in response to user input — I’ve rolled my own CascadingDropDown. When I get the new SELECT back from the server the first thing I do is remove the existing extender from the existing SELECT, then I replace the old SELECT with the new one that I’ve just received, and then I dynamically instantiate a new ListSearch extender:
function OnGetListSucceded(list) { var theListDiv = $get(‘TheListDiv’); // Contains TheList var theList = $get(‘TheList’); // An HTML SELECT RemoveListSearchFrom(theList); theListDiv.innerHTML = list; theList = $get(‘TheList’); AddListSearchTo(theList); }
Sometimes we have to do things that we know are wrong.
For example, in my current project we have to present a list of customers so that the user can select one or more of them. I work for a large company that has been around for many many years, and the list of customers can exceed 15,000 for some countries.
But I can’t decide which ones should be shown, and which ones should not be shown. Instead I need to show the complete list to the user. What is painful is that it can take a bit of time for this list to be sent down to the browser. It would be much better would be if the customer list could be cached on the browser’s document cache. This is what this blog post is about.
Note that in order for this to work, the list is presented as a simple HTML SELECT, and the selected items need to be processed correspondingly on the server using the Request.Form[SelectName] property.
In the ASPX page, I define my customer list like this:
<div id=”ListDiv”> <select id=”List” multiple=”multiple” size=”15″ disabled=”disabled”> <option>Loading List …</option> <option>This may take a moment the first time…</option> </select> </div>
Note that the SELECT is surrounded by a DIV, so that I can replace the DIV contents with a new list.
Then I have some JavaScript that fires when the page loads, and asynchronously fetches the list:
<script language=”javascript” type=”text/javascript”> // This is called by the ASP.NET AJAX Framework automatically function pageLoad() { var wRequest = new Sys.Net.WebRequest(); wRequest.set_url(‘ListHandler.ashx?Version=<%=ListVersion.Value%>)’); wRequest.set_httpVerb(“GET”); // GETs can be cached, POSTs can not wRequest.add_completed(OnFetchListCompleted); wRequest.invoke(); }
The pageLoad function gets invoked automatically. It simply fires off an HTTP GET request to a handler, passing as a parameter the value of the ListVersion hidden field. By changing the value of this field, the server-side code can control when the browser-cached contents are expired, and a new version is fetched (since changing the URL will mean there is no browser-cached version).
When the response comes back, it replaces the contents of the DIV with the value that is returned from the handler:
function OnFetchListCompleted(executor, eventArgs) { var listDiv = $get(‘ListDiv’); if(executor.get_responseAvailable()) { var list = executor.get_responseData(); if(list && list.startsWith(‘<select’)) { listDiv.innerHTML = list; } } } </script>
The handler simply generates the appropriate content:
<%@ WebHandler Language=”C#” Class=”ListHandler” %> using System; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Collections.Generic; public class ListHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.Clear(); context.Response.ContentType = “text/html”; context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetExpires(DateTime.Now.AddMonths(1)); context.Response.Cache.SetSlidingExpiration(true); HtmlTextWriter htmlTextWriter = new HtmlTextWriter( context.Response.Output); ListBox listBox = GetListBox(); listBox.RenderControl(htmlTextWriter); htmlTextWriter.Flush(); context.Response.End(); } private static ListBox GetListBox() { ListBox listBox = new ListBox();
...
return listBox;
}
...
Note the bolded directives to enforce the browser-side caching of the response.
I have a complete example here.
You might be wondering why I’ve used the Sys.Net.WebRequest mechanism with the HttpHandler. Why not simply use a Web Service? That would indeed be simpler, however there is no easy way to set the appropriate cache expiration headers, although it is possible if you are willing to use reflection — the PageFlakes guys use it to speed up their pages.
When a ListBox that has an associated ListSearch Extender is first used, the ListSearch Extender does some one-time initialization.
It delays this work this for very good reasons, as Ted Glaza explains here.
The extender needs to decide whether it can use the very fast binary search when the user types, or whether it must use the slower linear search. It does this by determining if the list is sorted, which can take a long time if there are thousands and thousands of entries in the ListBox.
The best thing would be to somehow give the ListSearch Extender a hint as to whether the ListBox is already sorted or not.
You can do this by making a couple of changes to the ListSearch extender. If you’d like to see this behaviour become a standard part of the ListSearch Extender, please vote for it and I’ll add it if there is sufficient interest.
Modifying the ListSearch Extender is extremely simple. First download the AJAX Control Toolkit, and then open the AjaxControlToolkit.sln
Open the behaviour JavaScript file “ListSearchBehavior.js”. At the top of the file you’ll see some properties.
Add a couple of new property:
// Properties
this._isSortedHintSet = false;
this._isSortedHint = false;
…
These indicate whether the user has told the extender if the list is sorted or not, and if so, whether it is sorted.
Next find the “_isSorted” function, and at the start of it (after the documentation comment), add this code:
if(this._isSortedHintSet) {
return this._isSortedHint;
}
…
Finally you need to expose this property so that it can be set from outside the class, although you could actually set the _isSortedHintSet and _isSortedHint from JavaScript, this is considered very bad form since the underscore prefix indicates they are private.
Define a getter and setter accessor at the end of the bevahior, where you’ll find the other accessors:
get_isSortedHint : function() {
/// <value type=”AjaxControlToolkit.ListSearchPromptPosition”>
/// Where the prompt should be positioned relative to the target control.
/// Can be Top (default) or Bottom
/// </value>
return this._isSortedHint;
},
set_isSortedHint : function(value) {
if (!this._isSortedHintSet || this._isSortedHint != value) {
this._isSortedHintSet = true;
this._isSortedHint = value;
this.raisePropertyChanged(’isSortedHint’);
}
}
If you add these two functions after the existing last function (which is “set_raiseImmediateOnChange” in the current build), then you must be sure to add a comma after the closing brace of this last function:
…
},
get_isSortedHint : function() {
….
Build the solution, and make sure projects that use the AJAX Control Toolkit now reference the AjaxControlToolkit.dll in AjaxControlToolkit\AjaxControlToolkit\bin\Debug
Now you can set this property in JavaScript when your page first loads, so that the extender does not have to work out whether the list is sorted:
<form id=“form1″ runat=“server”> <asp:ScriptManager ID=“SM1″ runat=“server” /> <asp:ListBox ID=“LB1″ runat=“server”> <asp:ListItem>Hello</asp:ListItem> </asp:ListBox> <cc1:ListSearchExtender ID=“LSE1″ runat=“server” TargetControlID=“LB1″></cc1:ListSearchExtender> <script language=“javascript” type=“text/javascript”> function pageLoad(sender, args) { var extender = $find(‘LSE1′); if(extender) { extender.set_isSortedHint(true); } } </script> </form>
This is the complete code (no changes to the code-behind file).
The pageLoad function is called automatically by the ASP.NET AJAX Framework (see http://ajax.asp.net/docs/overview/AJAXClientEvents.aspx) when all controls within the page have been loaded.
The function first finds the ListSearch Extender, and then gives it a hint that the list that it is targeting is actually sorted.
If you wanted to be able to set the property from the server side, instead of from JavaScript, you can edit the ListSearchExtender.cs file, and add the appropriate declaration of the property:
[ExtenderControlProperty]
[ClientPropertyName("isSortedHint")]
[DefaultValue("")]
public string IsSortedHint
{
get { return GetPropertyValue(“isSortedHint”, “”); }
set { SetPropertyValue(“isSortedHint”, value); }
}
Hiding the ListSearch Extender Prompt Message
June 19, 2007
You can hide the ListSearch Extender’s prompt message, so that it does not show, by setting the style to “display:none”, like this.
Define a CSS class in a StyleSheet”StyleSheet.css”:
.DontShowListSearchPrompt
{
display:none;
}
Then in your ASPX use the Style for the ListSearch Prompt:
<head runat=”server”>
<title>Don’t show ListSearch Extender Prompt</title>
<link href=”StyleSheet.css” rel=”stylesheet” type=”text/css” />
</head>
<body>
<form id=”form1″ runat=”server”>
<asp:ScriptManager ID=”SM1″ runat=”server” />
<asp:ListBox ID=”LB1″ runat=”server”>
<asp:ListItem>Hello</asp:ListItem>
</asp:ListBox>
<cc1:ListSearchExtender ID=”LSE1″ runat=”server”
PromptCssClass=”DontShowListSearchPrompt“
TargetControlID=”LB1″>
</cc1:ListSearchExtender>
</form>
</body>
</html>
Automatically attaching Extenders to Controls using Control Adapters - demo posted
February 28, 2007
I get a lot of hits on this article I posted last year, showing how you can use ASP.NET Control Adapters to automatically attach Control Extenders to a specific kind of ASP.NET control throughout an existing web site, without modifying any of the existing pages.
It uses a Control Adapter to create and attach extenders to the target controls, and also an HttpModule to ensure that all pages have a ScriptManager or ScriptManagerProxy.
Anyway, I finally got around to uploading a demo project showing this. It is slightly different to the article, since it shows how to attach a DropShadow extender to all UpdatePanels on your site. Not something you will necessarily want to do, but you get the point.
The demo project is here. It consists of a class project containing the Control Adapter and Http Module, and then a sample web site with a .browsers file to cause the adapter to be used, and a modified web.config to let the Http Module do its stuff.
Calling AJAX Control Extenders from JavaScript code
February 28, 2007
I am a big fan of declarative programming. It reduces the risk of errors, saves on mundane coding and lets me say what I want to do and someone else can figure out the how (although I like to understand the how too :-).
That said, when you find yourself doing strange and unnatural things simply to be able to use declarative programming then you know you’ve gone too far.
I found myself in this kind of situation recently. I have a very large GridView and for one of the columns I wanted to display a ModalPopup whenever the user clicked on the corresponding item in each row, where they could select information related to that item.
What I could have done is to hook up an instance of the ModalPopup in the template, so that for each row a new ModalPopup would be created.
Instead what I did was to define a single ModalPopup outside of the GridView, targeted at a dummy hidden LinkButton (with no label):
<asp:LinkButton ID=”DummyHiddenLinkButton” runat=”server”></asp:LinkButton>
<cc1:ModalPopupExtender ID=”SelectYxzModalPopupExtender“
runat=”server”
TargetControlID=”DummyHiddenLinkButton”
PopupControlID=”SelectYxzPanel”
OkControlID=”SelectYxzOKButton” >
</cc1:ModalPopupExtender>
Then in the template I hooked up the onclick method of the item to invoke a JavaScript function passing as a parameter key information related to the item.
Inside my JavaScript function I first initialize the Panel with information related to the item (in my case by invoking a page method), and then I programatically cause the ModalPopup to show:
function OnItemLinkClicked(someId) {
PageMethods.SomePageMethod(someId,
OnSomePageMethodSucceded,
OnSomePageMethodFailed);
}
function OnSomePageMethodSucceded(result) {
var extender = $find(’SelectYxzModalPopupExtender’);
var someList = $get(’SomeListInTheModalPanel’);
if(!extender || !someList) { …
}
someList.options.length = 0;
for(var i = 0; i < result.length; i++) {
var option = document.createElement(’OPTION’);
option.text = result[i].Text;
option.value = result[i].Value;
someList.options.add(option);
}
extender.show();
}
I’ve shown my call to a backend page method for completeness. I’m invoking a page method to get an array of structs related to the item that the user clicked, and then using the resulting array to populate a SELECT that I display within the modal popup.
But the interesting stuff is in bold. Here I am programatically finding my ModalPopup extender (using the $find shortcut to the Sys.Application.findComponent method), and then invoking its show method to cause it to display. The hidden LinkButton that is its “official” target never actually gets used.
I discovered what methods were available on the ModalPopup extender by looking at its source, which is available for all the control extenders when you download the AJAX Control Toolkit. Methods that are prefixed with an underscore are internal methods and I’d never call those, but I’m pretty comfortable calling the other methods, such as the handy show method above.
Last Tuesday I gave a presentation in Geneva on ASP.NET AJAX and the AJAX Control toolkit, and this morning I created a one hour screencast of the presentation available here (25MB) zipped, or here (33MB) as a WMV.
I tried to make it useful for someone that has at least dabbled in ASP or ASP.NET, and wants a quick bootstrap into Microsoft’s AJAX offerings.
It includes introductions to:
- AJAX
- ASP.NET AJAX including coding demos of the UpdatePanel (including a look behind the scenes), Web Services, and the JavaScript extensions
- The AJAX Control Toolkit including coding demos of using a couple of the control extenders
- Creating AJAX Control Extenders including a coding demo of creating your own extenders
It is encoded at quite a high level of compression so the sound is a little distorted but perfectly understandable.
Note:I recorded this around 6am this morning, and around 55 minutes into it my three year old and five year old sons decided to make their own (very sweet) contributions to the screencast — please forgive my momentary distraction, and when you hear me tell my three year old that he can take the money he found: he is talking about two small coins I gave him yesterday. This is not an attempt at bribary (honest!).
Many thanks to Atif Aziz and Dominique Kuster for organizing last week’s presentation, and Dicomp Acadamy Suisse Romande for hosting it.