Thursday, September 1, 2011

Silverlight 5 RC Released - Using PInvoke

It's exciting to see the progress of Silverlight 5 toward the final release that has been slated towards the end of the year. Today, the Silverlight Team announced the release of the Silverlight 5 Release Candidate. While it does not yet have a go-live license, it integrates many new features that were not available with the beta release, including the new PivotViewer integration and vector-based printing.

One of the new features included with the Silverlight 5 RC is the ability to interact with Platform Invocation Services, or PInvoke for short. This functionality allows managed code, like your Silverlight application, to make calls to unmanaged code like the C++ code that exists in DLLs on your system. PInvoke can be quite complex and covers a lot of different scenarios, so I've focused on a simple scenario to help you get started. You can learn more about PInvoke by reading this MSDN tutorial.

In this post, I will show you how to build a Silverlight Out-of-Browser (OOB) application that uses the native DLLs on your machine to play "beeps." With modern versions of Windows 7, the beeps have evolved into complex sounds that you can assign to various functions. Create a new Silverlight application called "PInvoke," set it to run out of browser and be sure to check the box that indicates it requires elevated trust.

Take a look at the MessageBeep function. It is documented here. The first thing you'll notice is that the type of beep or sound to play is not very friendly as it is passed in as an unsigned integer. Based on the documentation, you can create the following enumeration to simplify things and expose a few key types:

public enum BeepTypes : uint
{
    Ok = 0x00000000,
    Error = 0x00000010,
    Warning = 0x00000030,
    Information = 0x00000040
}

To make it easy to take a string and cast it to the enumeration value, add this extension method as well:

public static class BeepTypeExtensions
{
    public static BeepTypes AsBeepTypeEnum(this string beepType)
    {
        BeepTypes beepTypeEnum;
        return Enum.TryParse(beepType, true, out beepTypeEnum)
                    ? beepTypeEnum
                    : BeepTypes.Error;
    }
}

It is always good practice to encapsulate your calls to unmanaged code in a managed class. This limits the exposure and dependency on PInvoke. It also allows you to write more portable code because you can implement the calls in managed code or simply stub out empty methods for targets that don't support the PInvoke implementation. Here is a simple contract to play a sound:

public interface ISoundPlayer
{
    void PlaySound(BeepTypes type);
}

Now for the implementation. Create a class called SoundPlayer. The documentation gives you the message signature and the DLL that the method is located in. Bridging to the method in this example is very easy. First, include a using statement for System.Runtime.InteropServices. This is where the PInvoke bridge lives. Next, simply define the message signature as a static extern and import the DLL:

[DllImport("User32.dll")]
private static extern Boolean MessageBeep(UInt32 beepType);

Implement the contract and call the method you just imported using PInvoke:

public void PlaySound(BeepTypes type)
{
    if (!MessageBeep((UInt32) type))
    {
        throw new Exception("Beep failed!");
    }
}

That's all there is to it! Now wire up some Xaml in the main page to host four buttons:

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="1*"/>
        <RowDefinition Height="1*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"/>
        <ColumnDefinition Width="1*"/>
    </Grid.ColumnDefinitions>
    <Button x:Name="Ok" Content="OK" Grid.Row="0" Grid.Column="0"  Click="Button_Click"/>
    <Button x:Name="Error" Content="Error" Grid.Row="0" Grid.Column="1"  Click="Button_Click"/>
    <Button x:Name="Warning" Content="Warning" Grid.Row="1" Grid.Column="0"  Click="Button_Click"/>
    <Button x:Name="Information" Content="Information" Grid.Row="1" Grid.Column="1"  Click="Button_Click"/>
</Grid>

Make sure that the name of the button matches the name of the enum. In the code-behind for the main page, create an instance of the sound player:

private readonly ISoundPlayer _soundPlayer = new SoundPlayer();

Now you can implement the button click event. Simply cast the name of the button to the enumeration and call the method on your managed class:

private void Button_Click(object sender, RoutedEventArgs e)
{
    var button = sender as Button;
    if (button == null)
    {
        return;
    }
    _soundPlayer.PlaySound(button.Name.AsBeepTypeEnum());
}

That's it! Run the application in OOB mode and you will be able to press the buttons and hear your default Windows sounds play. If you change your theme or override the sounds, the application will play the correct sounds because it is bridging to the unmanaged code and asking the operating system to play the sound rather than trying to play it from a resource embedded within the Silverlight application.

Hopefully this light example will help you get started. PInvoke has a number of uses from interfacing directly with USB ports to enumerating drives and devices on the host system. It certainly opens the door to a new realm of possibilities with Silverlight.

Download the sample solution here (but remember, it requires that you install the Silverlight 5 RC first!).

Jeremy Likness

4 comments:

  1. Dear Jeremy,

    thanks for this blog post, I am also delighted to see that P/Invoke finally has made it into Silverlight.

    Along similar lines, I have found out that unsafe code can be built and run (OOB, elevated trust) with Silverlight 5 RC.

    In case you are interested, I have written a blog post on this: http://cureos.blogspot.com/2011/09/unsafe-support-in-silverlight-5.html

    I have not seen unsafe support being announced anywhere, and you obviously have to "cheat" a little to access it. Maybe you know more?

    Best regards,
    Anders

    ReplyDelete
  2. Hi again Jeremy,

    I am currently trying out the P/Invoke functionality in OOB mode. As far as I have been able to tell, I need to place my native DLL in the system path.

    If I bundle my native DLL as "Content" in the XAP file, I get a DllNotFoundException.

    Is this expected? Maybe I am missing something fundamental, but I thought the XAP file is also used when running OOB?

    Thanks in advance!
    Anders

    ReplyDelete
  3. Hi Anders,

    if you have found a solution to your problem please let me know! I want to run some third party DLL's with my Silverlight app - does that mean my clients and I will have to place those DLL's in the C:\Windows\System32 path?

    Thanks,
    Andrew

    ReplyDelete
  4. Hi Andrew,

    just today I have created a workaround for this problem. You can find all the details in this blog post: http://cureos.blogspot.com/2011/09/pinvoke-bundling-native-dlls-in.html

    Please try it out and do not hesitate to report if my workaround managed to solve your problem.

    Regards,
    Anders

    ReplyDelete