Department of InformatiX
Microsoft .NET Micro Framework Tools & Resources

Universal Configuration Tool Extensions

My message to hardware vendors:
Do not target AnyCPU, use x86. Otherwise the extension will not work on 64-bit, because .NET Micro Framework assemblies are x86 only.

The only thing you can extend so far is the cofiguration definitions and templates. If you would like to extend another parts of the application (tree, command bar, task list, etc.), feel free to let me know. To illustrate the extensibility of this tool, I am sharing the source code for Device Solution's MDB1 configuration.

Configurations are basically (as everyting else) arrays of bytes. Usually, the bytes represent some serialized structure, but do not contain any information on how does the structure look like. Therefore the most important purpose of your extension is to define this structure.

Configurations are stored in special area of device's persistent memory, and they are identified by name only. Because no current vendor identifies their devices, the configuration name is the only thing we can work with. So if two vendors happen to select the same name for a configuration, you can't do anything* in the definition what could help decide to which configuration it belongs. Hopefully, this will improve in the future.

Step 1. Getting the native structure

To start creating your extension, you first need to find out how the configuration structure looks like on the native side. The hardware vendor whould be able to supply you such information. This is what I have received from Device Solutions regarding the MDB1 configuration:

typedef enum { transportNone = 0, transportCom1 = 1, transportCom2 = 2, transportUsb = 3, transportTcpIp = 4 } MeridianTransportType; typedef struct { MeridianTransportType DebuggerPort; MeridianTransportType MessagingPort; MeridianTransportType DebugTextPort; MeridianTransportType stdioPort; } MERIDIAN_TRANSPORT;

Now check what returns the new Microsoft.NetMicroFramework.Tools.MFDeployTool.Engine.MFConfigHelper(mfDevice).FindConfig("MDB1") method in the MFDeployEngine.dll assembly:

SG SG SG SG HC HC HC HC DC DC DC DC 00 00 00 04 (signature, header CRC, data CRC, data length) M D B 1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (configuration name, up to 64 bytes long) 03 03 03 03 (configuration data)

All configurations are prefixed with common header, containing configuration's name, size, CRC checks and other information. You can see its structure in the same assembly, at Microsoft.NetMicroFramework.Tools.MFDeployTool.Engine.HAL_CONFIG_BLOCK. The important thing we have learned from this is that the underlaying type of MeridianTransportType enum is single byte.

Step 2. Defining the managed structure

Now we need to represent this structure in managed world, so that the data could be deserialized into it. As we have seen, the data contains a header, which has to be part of the structure for successfull deserialization. Microsoft made this part easier by introducing a Microsoft.NetMicroFramework.Tools.MFDeployTool.Engine.IHAL_CONFIG_BASE interface:

public interface IHAL_CONFIG_BASE { HAL_CONFIG_BLOCK ConfigHeader { get; set; } // Common header as noted above int Size { get; } // Size of the structure }

Not sure what were the intentions with the Size property - it is being used when computing CRC, but marshaller's measurements are used for memory allocation during serialization. If anybody knows some background, I would be curious. The ConfigHeader is a property too, and properties are not used during de/serialization, so we need to include the header ourselves (interfaces cannot contain fields). Knowing all this, creating the managed structure should be pretty strightforward:

using System; using System.Runtime.InteropServices; using Microsoft.NetMicroFramework.Tools.MFDeployTool.Engine; // requires reference to MFDeployEngine assembly namespace DeviceSolutions.SPOT.Configuration { [StructLayout(LayoutKind.Sequential)] public struct MdbConfigData : IHAL_CONFIG_BASE { public HAL_CONFIG_BLOCK Header; public Byte DebuggerPort; public Byte MessagingPort; public Byte DebugTextPort; public Byte StdioPort; HAL_CONFIG_BLOCK IHAL_CONFIG_BASE.ConfigHeader { get { return Header; } set { Header = value; } } int IHAL_CONFIG_BASE.Size { get { return Marshal.SizeOf(this); } } } }

Tip 1: If you have unsafe code allowed, you can use return sizeof(MdbConfigData) instead of the marshaller.

Tip 2: If the native structure contains an array buffer (for example string must be defined this way), you can declare it like this one:

public const int MaxMacBufferLength = 64; public unsafe fixed byte MacAddressBuffer[MaxMacBufferLength];

Note: The debugger engine actually allows to write data only structures (automatically adding the header), but I strongly encourage you to use the IHAL_CONFIG_BASE as the engine does not allow you to deserialize to such structures (no automatical header stripping).

Step 3. Creating the wrapper class (optional)

So far we have defined the raw representation of data. However, this definition must have fixed, matching structure of fields and all of the types used must be serializable. Creating a wrapper allows you not only to make it more friendly and safer to use for others, but mainly allows to introduce some extra logic to the configuration, like values checking and coercion, or to use any type you need. Technically, except the usage of non-serializable types or inheritance, this step is not required - properties and methods can be defined on the structure itself, but I personally believe it is a good practice to separate raw data definition and its meaning and logic.

In our case, we would prefer to expose a predefined number of values for the debugging transport type selection, instead of raw byte value. For this reason, we define the following enum (with values and type matching the native declaration):

namespace DeviceSolutions.SPOT.Configuration { partial class MdbConfig // our wrapper class, this is nested enum { public enum MeridianTransportType : byte { None = 0, Com1 = 1, Com2 = 2, Usb = 3, TcpIp = 4 } } }

This can be then used in the wrapper class:

public partial class MdbConfig { public MdbConfigData MdbData; // raw data reference public MeridianTransportType DebuggerPort { get { return (MeridianTransportType)MdbData.DebuggerPort; } set { MdbData.DebuggerPort = (byte)value; } } ... }

Also, setting each individual port (messagging, debugger, etc.) to different value is very unusual, so we will add a common property, which will set all ports to the same value. As we will see later, this will come very handy for building the user interface:

public MeridianTransportType? CommonPort { get { MeridianTransportType transportType = DebuggerPort; if (MessagingPort == transportType && DebugTextPort == transportType && StdioPort == transportType) return transportType; return null; // return null if values differ } set { if (value == null) return; // do nothing if no port selected DebuggerPort = MessagingPort = DebugTextPort = StdioPort = value.Value; } }

Step 4. Pairing with configurations

The only thing remaining for our code to work with the configuration tool is to somehow pair our wrapper (and/or data structure) with the configuration name it belongs to. For this purpose, I have chosen some built-in attributes from the .NET Framework, from which the most important one is the System.ComponentModel.DisplayNameAttribute. If you are not using wrapper, add it to the data structure. If you do, add it to the wrapper class instead, and specify name of the member holding raw data reference using DefaultBindingPropertyAttribute:

using System.ComponentModel; namespace DeviceSolutions.SPOT.Configuration { [DisplayName("MDB1"), DefaultBindingProperty("MdbData")] public partial class MdbConfig { public MdbConfigData MdbData; // raw data reference public MeridianTransportType DebuggerPort { get { return (MeridianTransportType)MdbData.DebuggerPort; } set { MdbData.DebuggerPort = (byte)value; } } ... } }

Voilà! Place the assembly into the tool's directory and this is all what is required for the tool to pick it up. As you can see, you don't need any references to 3rd party assemblies, no changes or specialized additions to the code, no registration or hooking into the tool, no versioning issues. You can just compile such configuration data (and wrapper) into your SDK library!

Step 5. Improving the property grid experience

All (non-indexed) public properties of the wrapper (resp. data structure) are listed in the property grid:

Property grid for LCD1 configuration

(In the current release, collection properties are also ignored.) There are couple of tricks how this view can be customized:

Step 6. Supplying the graphical template

We are getting to the best part of the process: you can define a graphical interface for your configuration. This should be an easy and quick way to set the most important settings. This is how our debugging transport selection template looks like:

Nice, isn't it? Okay, to start, you need to add a WPF resource dictionary to the library. This can be a bit tricky if you have created new, standard C# library, which does not support adding a WPF items (even in Visual Studio 2010 beta 2!). You need to add {60dc8134-eba5-43b8-bcc9-bb4bc16c2548} to the ProjectTypeGuids property of your project file, see Building WPF Emulators article for details, under Emulator → Whatever.

As soon as you can add WPF items to the library project, add a new Resource Dictionary. What you need is to create a keyless DataTemplate for the wrapper (resp. data structure) type. This will be then automatically loaded by WPF and displayed in the tool window, which is pretty cool. You can do whatever you want in there, from rich tooltips to catchy animations, if you where that kind of hardware vendor, of course. Your parent element is directly the TabItem element.

Actually, using buttons (or better, toggle buttons) for the transport selection would require some code behind, and tremendous work to synchronize their states with the current setting. Sure this super simple configuration can be done without any code! Let's take advantage of the CommonPort property we created above, and bind a standard list box to it:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:DeviceSolutions.SPOT.Configuration" xmlns:s="clr-namespace:System;assembly=mscorlib"> <DataTemplate DataType="{x:Type c:MdbConfig}"> <!-- keyless data template for our wrapper type --> <DataTemplate.Resources> <ObjectDataProvider x:Key="MeridianTransportTypeValues" ObjectType="{x:Type s:Enum}" MethodName="GetValues"> <ObjectDataProvider.MethodParameters> <x:Type TypeName="c:MdbConfig+MeridianTransportType" /> </ObjectDataProvider.MethodParameters> </ObjectDataProvider> </DataTemplate.Resources> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock Margin="0,0,0,5" FontSize="18" TextBlock.FontWeight="Bold">Debug Transport Selection</TextBlock> <ListBox ItemsSource="{Binding Source={StaticResource MeridianTransportTypeValues}}" SelectedItem="{Binding CommonPort}" BorderThickness="0"> </ListBox> </StackPanel> </DataTemplate> </ResourceDictionary>

I don't know how many experienced WPF developers are in the .NET Micro Framework community, but I hope that plenty! And I encourage those who are not, like me, to become at least beginners - the .NET Micro Framework UI model is based on WPF as well, so it will help you with device development, and mainly to understand the technology and its philosophy, its strengths and limits. Anyway, here we are filling the list box directly using all values of the enum, with a little help of System.Enum.GetValues(typeof(MdbConfig.MeridianTransportType)) method. If some guy decides to implement debugging over SPI, just defining its value in the enum will automatically offer it in the template for selection - without touching it.

Some probably already know what will come next. We will modify the list box item template to look like toggle buttons with illustrations, to have a bit nicer and easy looking UI. Just between the <ListBox ...> and </ListBox> elements, insert the following snippets of code:

  1. This will replace text and blue selection background with squared toggle buttons, synchronized with the selected item:
    <ListBox.Resources> <Style x:Key="{x:Type ListBoxItem}" TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBoxItem"> <ToggleButton MinWidth="64" MinHeight="64" Focusable="False" IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Margin="3,0"> <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"/> </ToggleButton> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.Resources>
  2. This one will arrange the the buttons horizontally instead of vertically:
    <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </ListBox.ItemsPanel>
  3. And the last one will replace text with an image above the text:
    <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <Image Source="{Binding Converter={StaticResource EnumImageResourceConverter}}" HorizontalAlignment="Center" /> <TextBlock Text="{Binding}" HorizontalAlignment="Center" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate>

The image part requires a bit more attention. First, notice that you can use value converters and other XAML features (e.g. custom markup extensions) in your templates without problems. Here we have an enum value and need to get an image representing it. Our enum values are None, Com1, Com2, Usb and TcpIp. So I found four images:


None.png

Com.png

Usb.png

TcpIp.png

and placed them into the library's Resources folder. Their build action must be set to Resource. The file names were chosen intentionally, so that the converter can just strip numbers from the enum name (to have the same image for all com ports in our case) and use it to load the image. Remember, that our library is loaded by the tool, so we have to use the full pack://application:,,,/AssemblyName;component/ResourceName URI to reference its resources.

The value converter can then look like this:

using System; using System.Windows.Data; namespace DeviceSolutions.SPOT.Configuration { class EnumImageResourceConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value == null) return System.Windows.DependencyProperty.UnsetValue; string valueString = value.ToString(); // convert enum value to its name int nameLength = valueString.Length; // remove trailing digits while (nameLength > 0) if (!char.IsDigit(valueString, --nameLength)) break; System.Diagnostics.Debug.Assert(nameLength > 0); // something went wrong // return ConfigImageExtension.ProvideValue("Resources/" + valueString.Substring(0, nameLength + 1) + ".png"); // (see bonus chapter) return new Uri(string.Format("pack://application:,,,/DeviceSolutionsConfigurationLibrary;component/Resources/{0}.png", valueString.Substring(0, nameLength + 1)), UriKind.Absolute); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }

We just need to instantiate it in the XAML - add this line into the DataTemplate.Resources element:

<c:EnumImageResourceConverter x:Key="EnumImageResourceConverter" />

And that's it. All files can be named as you wish, placed anywhere in the project's folder structure.

Bonus chapter. Referencing images

In the value converter above, we hardcoded the assembly name in. If the assembly is renamed, or if the image is not found, or its loading fails, the tool crashes. Since images are pretty common object in the user interface, let's create this generic markup extension:

using System; using System.Windows.Markup; using System.Windows.Media; namespace DeviceSolutions.SPOT.Configuration { [MarkupExtensionReturnType(typeof(ImageSource))] public class ConfigImageExtension : MarkupExtension { static string ThisAssemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; static ImageSourceConverter ImageSourceConverter = new ImageSourceConverter(); public ConfigImageExtension() { } public ConfigImageExtension(string path) { Path = path; } public override object ProvideValue(IServiceProvider serviceProvider) { return ProvideValue(Path); } public static object ProvideValue(string path) { string uriString = string.Format("pack://application:,,,/{0};component/{1}", ThisAssemblyName, path); try { return ImageSourceConverter.ConvertFrom(new Uri(uriString, UriKind.Absolute)); } catch { return null; } } public string Path { get; set; } } }

This one works with any assembly name, so feel free to put it in your namespace and use it in your libraries. See comments in the value provider how to call this from code, see the next line how to use this for static images directly in XAML:

<Image Source="{c:ConfigImage Resources/YourImage.png}" />

(where c is the namespace of the markup extension) Almost as easy as using the standard image path, isn't it?

What if my configuration data requires connection to the device?

If your configuration requires being connected to the device (for example if you need a deployment key), you'd probably have to wait until next release. And since I've still not decided how to pass the connection to extensions, you can share your needs or expectations with me.

Troubleshooting

I'm likely going to improve the extensions failure diagnostics in the future. For now, I am able to supply you a debug build, which outputs couple of things, so you should be able to find out why is your extension not being loaded for example. If you have any troubles or questions with your extension development, feel free to ask!

Finally, this application is intended for you. Should you have any suggestions for future releases, I will be glad to hear from you.

Comments
Sign in using Live ID to be able to post comments.