Archive for the ‘Uncategorized’ Category

5e vlinderdag – Marieke de Koning

www.mariekedekoning.nl

Code Snippet: change background color of Visual Studio editor with code

When working on a project which has a trunk and branches, it happens sometimes that I am changing code at the wrong location. To make it clear that I am working on a branch I added some VB Macro to the Visual Studio IDE which changes the background color when the solution is stored in a directory called “branches”.

Sub ChangeBackgroundColorForBranchProjects() Handles SolutionEvents.opened, DocumentEvents.documentopening
        Dim backgroundColor As UInt32
        backgroundColor = &HFFFFFF
        If (DTE.Solution.FullName.ToLower.Contains("branches")) Then
            backgroundColor = &HC0FFFF
        End If

        CType(DTE.Properties("FontsAndColors", "TextEditor").Item("FontsAndColorsItems").Object, EnvDTE.FontsAndColorsItems).Item("Plain Text").Background = backgroundColor
    End Sub

You can easily add above code to Visual Studio by using the Macro IDE. I put this code within the (by default exisiting) EnvironmentEvents file part of the MyMacros project.

This simple code snippet checks only if the solution fullname (which includes the path) contains the word “branches”. It is of course possible to write more intelligent behaviour which looks for example to a setting file within your solution. Just use this as a starting point. Hopefully it will reduce the number of times that you are changing code within the wrong solution.

Getting things done and Outlook – Keep original message attached to task or appointment

To get my email, tasks and mind organized, I am trying to use the Getting Things Done method developed by David Allen (English book/Dutch book). Because I am using Outlook as “external storage system”, I read an additonal book (Iedere dag je hoofd en inbox leeg (dutch book)) about implementing GTD within Outlook.

The main idea of this book is that you copy emails to task lists or your agenda during the processing phase with the use of some shortcuts. With the default Copy to folder functionality of Outlook, this works quite okey with two exceptions:

  • Attachments are not copied to a task/agenda item
  • You don’t have a reference to the original message, so you are not able to reply if you finished a task or need additional information

I developed a macro with some VBA code which will create a new Outlook item (appointment or task) and attaches the orginal message to this new item. This way I can always open the original email (with attachments) and sent a reply without looking up the email in the archive folder first.

Sub CopyEmailToNewItem()

    Dim objMailItem As Outlook.mailItem

    If (ActiveExplorer.Selection.Count = 1) And (ActiveExplorer.Selection.Item(1).Class = olMail) Then
        Set objMailItem = ActiveExplorer.Selection.Item(1)
    End If

    Dim NameSpace As Outlook.NameSpace
    Set NameSpace = Outlook.GetNamespace("MAPI")

    Dim selectedFolder As Folder
    Set selectedFolder = NameSpace.PickFolder

    Const attPath As String = "C:\temp\"

    Set objNewItem = selectedFolder.Items.Add(selectedFolder.DefaultItemType)
    With objNewItem
        .Subject = objMailItem.Subject
        .Body = objMailItem.Body

        objMailItem.SaveAs attPath & objMailItem.EntryID

        .Attachments.Add attPath & objMailItem.EntryID, olEmbeddeditem, , "Orginal message"

        Kill (attPath & objMailItem.EntryID)

        .Display
    End With
End Sub

Dam tot damloop 2010

19 september om 11:30 was het dan zo ver… de start van mijn eerste dam tot damloop. Het hardloopevenement waarvoor ik in maart met trainingen bij AV Zaanland was begonnen. Toen begon ik met 1 minuut hardlopen en was dan al kapot. Nu een half jaar verder heb ik er 16,1 km opzitten in 1 uur 39 minuten en 33 seconden.

Het meedoen aan de dam tot damloop is erg gaaf. Ik zelf vond het door te ijtunnel lopen toch wel een hoogtepunt van het parcours omdat ik daar enkel met de bus of auto doorheen ben gereden. Maar ook de hoeveelheid mensen langs de weg maakt een enorme indruk. Ik ga de training bij AV Zaanland doorzetten dus wellicht volgend jaar weer…

Uitslag
Video’s

WCF Data Contract Schema validation with Schematron

WCF is mostly used within a code first implementation. The data contract and service operations are implemented as methods within a class which are decorated with attributes (like [DataContract] and [DataMember]). With these attribute the types define that they are serializable with a serializer such as the DataContractSerializer. The DataContractSerializer is an optimized serializer which is faster than the XmlSerializer (http://www.danrigsby.com/blog/index.php/2008/03/07/xmlserializer-vs-datacontractserializer-serialization-in-wcf/). A consequent of this optimalization is that only a subset of the XML Schema (XSD) is supported: http://msdn.microsoft.com/en-us/library/ms733112.aspx

When working with existing systems, it is most times not possible to use the code first approach for the definition of the data contract. XSD and WSDL are exposed by existing services which makes client proxy and data contract code generation possible with the use of svcutil. By default svcutil tries to use to DataContractSerializer and falls back to the XmlSerializer when usage of the DataContractSerializer is not possible (it is possible to force the use of the data contract serializer by supplying the /serializer:datacontractserializer argument, which not falls back to the XmlSerializer but returns an error when it is not able to generate the code).

Within a situation that WSDL and XSD are leading but changes can be made to support the DataContractSerializer, it can be time consuming to update those service definitions files. The svcutil tool gives limited feedback when it fails to use the DataContractSerializer and it does not give you a clear overview of the issues that should be solved to make it compliant to this serializer.

Schematron

Schematron is a rule-based validation language for making assertions about the presence or absence of patterns in XML trees. It is a structural schema language expressed in XML using a small number of elements and XPath. (http://en.wikipedia.org/wiki/Schematron). With the use of a Schematron rule set file, additional conditions can be defined for the data contract schema definition.

Schematron (http://www.schematron.com/) is an ISO standard (http://standards.iso.org/ittf/PubliclyAvailableStandards/c040833_ISO_IEC_19757-3_2006(E).zip) and makes it possible to define constraints which cannot be expressed/defined within a XML Schema (XSD). It is possible to define rules which take related elements into account. Within a Schematron rule set file, rules are made by specifying assertions. An example of a rule

<sch:pattern id="schema-element-attribute-error">
	<sch:rule context="xsd:schema/xsd:element">
		<sch:assert test="@nillable = 'true'">Must be true for associated GEDs.</sch:assert>
	</sch:rule>
</sch:pattern>

Above rules makes sure that global element declarations (xsd:element directly defined within xsd:schema) should have the nillable=”true” attribute.

Besides <assert> there is also a <report> assertion. When the Schematron rule set is applied on an XML file, <assert> is logged when the condition fails (positive assertions). On the other side <report> is logged (within the output file) when the condition is met (negative assertions). For example when above rule fails on the supplied data contract schema definition, the output file (SVRL, Schematron Validator Report Language) will contain:

<svrl:active-pattern id="schema-element-attribute-error" name="schema-element-attribute-error"/>
	<svrl:fired-rule context="xsd:schema/xsd:element"/>
	<svrl:failed-assert test="@nillable = 'true'" location="/*[local-name()='schema' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='element' and namespace-uri()='http://www.w3.org/2001/XMLSchema'][1]">
		<svrl:text>Must be true for associated GEDs.</svrl:text>
	</svrl:failed-assert>
<svrl:fired-rule context="xsd:schema/xsd:element"/>

whereby the location of the element causing this failure is part of the result message (location attribute).

Applying an schematron ruleset

A Schematron rule set file is converted to an XSLT file which will be applied on the XML file on which the assertions should be executed. This is a pipelined process whereby the Schematron file is tranformed by different XSL files (and an XSLT engine) as shown in the diagram below

When using the basic elements of Schematron, only one transformation is needed before it can be applied on a file which should be validated. I used the Schematron XSLT1 implementation (http://www.schematron.com/tmp/iso-schematron-xslt1.zip) together with the free AltovaXML XSLT Engine (http://www.altova.com/altovaxml.html)

AltovaXML /xslt1 iso_svrl_for_xslt1.xsl /in datacontract.sch /out datacontract.xsl
AltovaXML /xslt1 datacontract.xsl /in <xsd schema which should be checked>  /out result.svrl

Data Contract Validator

With the use of Schematron it is possible to make a data contract serializer validator, which gives you a quick overview of issues to make use of the Data Contract Serializer possible. I developed a first implementation of this rule set based on the Data Contract Schema Reference (http://msdn.microsoft.com/en-us/library/ms733112.aspx). Not all rules are implemented yet.

When validating a data contract XSD there are errors (a Global Element Declarations should have @nillable=’true’) and warnings (attribute @id is ignored). To distinguish between both validation types I used for the errors, and for warnings/verbose information.

You can download the Data Contract Schematron Rule set at http://johandekoning.codeplex.com/SourceControl/changeset/view/48265#831939

Turing machine implementation with WF4 and FlowChart

A couple of days ago I received a tweet about a real hardware implementation of the Turing Machine (http://www.aturingmachine.com/). Impressed about this implementation, I was thinking about how to implement a simple Turing Machine counting program with the use of Workflow Foundation 4. And that’s where the story starts about my first experience with WF4.

Turing machine definition

At Wikipedia a definition of the Turing machine can be found: http://en.wikipedia.org/wiki/Turing_machine. To keep it short, a Turing machine consists of two parts

  • A tape of unlimited length which is separated in cells
  • A machine that can read/write on the tape

The machine is a finite state machine. The machine is in one state and with the use of a set of rules the machine can transfer to a different state. A rule contains the following parts

  • current state
  • symbol read
  • next state
  • symbol to write
  • move direction

The following rule [state: 0, read: blank, new state: 1, write: blank, move: left] will be executed by the machine when the current state is 0 and the value of the cell on the tape is blank. When the rule is executed, the machine will transfer to state 1, keeps the cell blank and will move the tape one cell to the left. A next rule for state 1 will be executed based on the new cell value read on the new tape position.

The machine part will be written with the use of a workflow diagram. This diagram will contain the different states and rule actions.

Counting rules

The rules for making counting possible (as taken from the aturingmachine.com website)

[state: 0, read: 1, new state: 0 write: 1, move: right]
[state: 0, read: 0, new state: 0, write: 0, move: right]
[state: 0, read: blank, new state: 1, write: blank, move: left]

[state: 1, read: 0, new state: 0, write: 0, move: right]
[state: 1, read: 1, new state: 1, write: 0, move: left]
[state: 1, read: blank, new state: 0, write: 1, move: right]

Tape implementation

The tape is a collection of cells which has an unlimited length. New cells can be added at the start or end of the written tape part. To support this functionality, an implementation is made based on a List collection making it possible to not only add new values at the end of the tape but also at the beginning.

public class Tape : ITape
    {
        public int Position { get; private set; }
        public int Length { get { return _internalCollection.Count; } }

        private List<int?> _internalCollection;

        public Tape()
        {
            Position = 0;
            _internalCollection = new List<int?>(){null};
        }

        public int? ReadCell()
        {
            return _internalCollection[Position];
        }

        public void WriteCell(int? cellValue)
        {
            _internalCollection[Position] = cellValue;
        }

        public void MoveCell(MoveDirection direction)
        {
            switch(direction){
                case MoveDirection.Left:
                    if (Position == 0)
                    {
                        _internalCollection.Insert(0, null);
                    }
                    else
                    {
                        Position--;
                    }
                    break;
                case MoveDirection.Right:
                    Position++;
                    if (Position == _internalCollection.Count)
                    {
                        _internalCollection.Add(null);
                    }
                    break;
            }
        }

        public override string ToString()
        {
            StringBuilder tapeValue = new StringBuilder("[");
            foreach (var cell in _internalCollection)
            {
                if (cell == null) { tapeValue.Append(" "); } else { tapeValue.Append(cell.ToString()); }
            }
            tapeValue.Append("] ");
            return tapeValue.ToString();
        }
    }

Flowchart

The state workflow of Workflow Foundation 3 is not part of version 4. The alternative given is the flowchart. With the flow chart certain types of state machine workflows can be implemented (but not all of them). A flowchart is powerful and simple to use, making it possible to loop back to previous executed activities. This is very useful for moving between the different states.

Together with the Flowchart, two activities are introducted: FlowDecision and FlowSwitch. A FlowDecision can be seen as an if condition while the FlowSwitch is like a switch statement in code. The FlowSwitch will be the central decision node for each state within the turing machine counting implementation

Workflow implementation with standard activities

A console workflow application is used for this implementation. An instance of the Tape is set as variable on the FlowChart.

The counting program can be developed with the use of the standard activities provided by WF4. The following activities are used

  • FlowSwitch: based on the cell value an execution path is chosen
  • InvokeMethod: used for executing the WriteCell and MoveCell methods
  • WriteLine: write the value of all cell values to the console.
  • Delay: this activiity is optional. Some delay to make output more readable during execution

The default path of the FlowSwitch activity is used for supporting the blank values. This is because of the fact that FlowSwitch works with string values, making it impossible to define a path for a null value.

Custom activities

The InvokeMethod activity is a generic activity to make execution of a method possible. It takes some time to configure each activity. The method parameters (for example the cell value which should be written) are implemented as a Parameter collection, which are not directly visible from the property editor within Visual Studio. To simplify/fasten this assignment process, custom activities will be developed to support the specific methods on the Tape instance.

Custom code activities are based on the CodeActivity or NativeActivity class. The CodeActivity is for creating simple synchronous custom activities. This model is suitable for the custom activities needed by the turing machine workflow. When you want to use all of the functionality exposed by the WF4 runtime, you should use the NativeActivity base class.

Two custom activities are developed

  • MoveCell activity: for moving the position on the tape one cell. The Direction property holds the move direction
  • WriteCell activity: for writing a cell value on the current position. The CellValue property holds the value which should be written
namespace TuringMachine.Activities
{
    [Designer(typeof(MoveCellDesigner))]
    public sealed class MoveCell : CodeActivity
    {
        [RequiredArgument()]
        public InArgument<ITape> Tape { get; set; }

        public MoveDirection Direction { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            var tape = context.GetValue(Tape);
            tape.MoveCell(Direction);
        }
    }
}
namespace TuringMachine.Activities
{
    [Designer(typeof(WriteCellDesigner))]
    public sealed class WriteCell : CodeActivity
    {
        [RequiredArgument()]
        public InArgument<Nullable<int>> CellValue { get; set; }

        [RequiredArgument()]
        public InArgument<ITape> Tape { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            var tape = context.GetValue(Tape);
            var cellValue = context.GetValue(CellValue);

            tape.WriteCell(cellValue);
        }
    }
}

The InArgument type used for the properties can hold (besides a value like a normal property) an expression. Those expression are defined using the VB.Net language (so null should be defined as Nothing)

The custom activities are added to the Workflow Toolbox making it possible to drag them into the workflow. You can change the property values from within the Property editor

Custom design

When viewing the diagram, it is still not directly clear which values are written and to which direction the position on the tape is moved. WF4 makes it possible to assign a design to a custom activity. The design of the activity is developed with XAML. Assignment is done with the use of the Designer attribute (as shown in the code above)

The XAML used for the MoveCell activity makes it possible to directly select the move direction from within the diagram

<sap:ActivityDesigner x:Class="TuringMachine.Activities.Designer.MoveCellDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:tm="clr-namespace:TuringMachine.Classes;assembly=TuringMachine.Classes">
    <sap:ActivityDesigner.Resources>
        <ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="MoveDirectionValues">
            <ObjectDataProvider.MethodParameters>
                <x:TypeExtension TypeName="tm:MoveDirection"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </sap:ActivityDesigner.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock VerticalAlignment="Center">Direction:</TextBlock>
        <ComboBox Grid.Column="1" ItemsSource="{Binding Source={StaticResource MoveDirectionValues}}" SelectedValue="{Binding ModelItem.Direction, Mode=TwoWay}"></ComboBox>
    </Grid>
</sap:ActivityDesigner>

And the WriteCell Activity makes the cell value assignment possible from within the workflow diagram. For the WriteCell activity an ExpressionTextBox is used, supporting the InArgument expression possibilities.

<sap:ActivityDesigner x:Class="TuringMachine.Activities.Designer.WriteCellDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation"
    xmlns:conv="clr-namespace:System.Activities.Presentation.Converters;assembly=System.Activities.Presentation"
    xmlns:local="clr-namespace:TuringMachine.Activities.Designer"
    xmlns:sys="clr-namespace:System;assembly=mscorlib">
  <sap:ActivityDesigner.Resources>
    <conv:ArgumentToExpressionConverter x:Key="expressionConverter"/>
  </sap:ActivityDesigner.Resources>
  <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock VerticalAlignment="Center">Cell value</TextBlock>
        <sapv:ExpressionTextBox Grid.Column="1" Expression="{Binding Path=ModelItem.CellValue, Mode=TwoWay, ConverterParameter=In, Converter={StaticResource expressionConverter}}" ExpressionType="{local:NullableExtension sys:Int32}" OwnerActivity="{Binding Path=ModelItem}"></sapv:ExpressionTextBox>
    </Grid>
</sap:ActivityDesigner>

Workflow implementation with custom activities

With the use of the developed custom activities together with their design, the diagram of the workflow will give us directly more information related to the execution of the program

What’s next?

My first impression about WF4 is that the usage is more simplified. Within the previous version of WF you had to write a lot of code to execute external methods. With the use of the CodeActivity (or NativeActivity) class it is easy to implement custom activities. The possibility to use XAML to layout your custom activities is very powerful. It makes it possible to implement diagrams which are visual more meaningful (especially useful when hosting the WF designer within your own application). While the FlowChart is not a real State Workflow, it is suitable for the implementation written above. The usage feels more natural, just like developing a sequence workflow.

While developing the counting workflow I had some issues with the support of Nullable types and the ExpressionTextBox. I needed to implement an TypeExtension to make it possible to define the ExpressionType as a Nullable. This was not what I expected because XAML 2009 (XAML version of .Net 4) should make it possible to define the type as {x:Type TypeName=System:Nullable`1[[System.Int32]]}. But this syntax is not accepted as valid.

Source code

The visual studio 2010 solution including the workflows and source code files can be found at (http://johandekoning.codeplex.com -> browse to WF/TuringMachine)

Powershell – Sync/shift file creation time

I went on holiday for two weeks together with my girlfriend. We both had our own camera and when we returned home we wanted to merge both photo collections. There was only a small problem, the date/time settings of the cameras were not in sync. This will give you funny/strange results when viewing the slideshow.

To solve this issue, I created a small powershell script. As an argument you provide a datetime string (format ddMMyyyyHHmmss). The oldest file (inside the directory where you execute this script) will get this new provided creation date. The creation date of the other files is updated taken the same time span (between oldest file date and the provided date) into account. Executing this script on one of the photo collections will make it possible to merge both collections.

$oldestFile = dir | sort-object CreationTime | select -first 1

$newStartDateTime = [datetime]::ParseExact($args,"ddMMyyyyHHmmss",$null)
$timespan = $newStartDateTime - $oldestFile.CreationTime

dir | foreach { $_.CreationTime = ($_.CreationTime + $timespan)}

My colleague went to a conference and all I got was this lousy eco-button

Today I got an eco button from my colleague. It is a hardware button with a green flashing light (is that eco?) which can be connected to your computer. With software installed, your computer will be put in stand-by mode when you press the button.

Because I use my computer the whole day for development, I don’t need an eco-button. I need an build project button for the Visual Studio development environment. A little hack is really easy to make and the result will be a more impressive build routine :)

Pressing the button (after connecting it to your computer without installing the ecobutton software) will execute the keys command

Windows-Key+R ecobutton ENTER

In my opinion a stupid implementation, because each time you press the button you see a Run dialog, followed by the “ecobutton” text filled in and finished with an enter. But this way it is quite easy to replace ecobutton.exe with your own application (keeping the same name)

So I developed a little console application (maybe a hidden application is even better) which will search my Visual Studio application and send the key command F6 (you can also send CTRL+SHIFT+B).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace WhatDoesThisButtonDo
{
    class Program
    {
        [DllImport("user32.dll")]
        static extern bool SetForegroundWindow(IntPtr hWnd);

        static void Main(string[] args)
        {
            IntPtr hWnd = IntPtr.Zero;

            foreach (var p in Process.GetProcesses())
            {
                if (p.ProcessName.Equals("devenv")) {
                    hWnd = p.MainWindowHandle;
                    break;
                }
            }

            if (hWnd != IntPtr.Zero)
            {
                SetForegroundWindow(hWnd);
                SendKeys.SendWait("{F6}");
            }
        }
    }
}

Explaining the code

When pressing the ecobutton, Visual Studio is not the active application anymore. Therefore I first have to find the window handler (of Visual Studio), make it the active foreground application and send the F6 key. There is a FindWindow method (user32.dll) but this one is based on the caption on the title bar. Because your solution name is part of this caption, the name is not static. To solve this issue I loop through the different processes to find the devenv process (when multiple visual studio’s are running, only one is used in this sample).

The SetForegroundWindow brings Visual Studio to the foreground. The F6 key is sent with SendKeys.SendWait. Instead of the F6 key you can also send the CTRL+SHIFT+B keystrokes by supplying “^+B”

To use this small applicaton, make sure it is called ecobutton.exe and placed within your PATH (otherwise Windows is unable to find it).

For me the Build button is way better than the Eco button version, but probaly you have better/other implementation suggestions what you can do with this hardware device. Please post them as feedback on this blogpost.

Top2OneNote Addin – Making my digital notepad notes searchable

Last week I bought a nice gadget at our discount supermarket (Aldi). The gadget is called a Digital notepad and makes it possible to record your notes while writing them down on paper. Your writing is stored on the internal memory as vector data. There is a lot of software supplied, but using them is not that straightforward.

Digital notepad

Most of the software supplied with this gadget wants to convert the note to clear text. But because of my bad handwriting, most of the time the converted text makes no sense. I also want to keep the drawing which I added to my notes. So instead of converting it to text, I would be happy if I can archive the notes and make them searchable.

Microsoft OneNote gives Tablet PC users the option to add handwritten notes to there OneNote notebook. Those notes are made searchable, while the handwritten note still exists. Instead of using a Tablet PC I want to convert my Digital Notepad content to OneNote handwritten notes. And therefore I developed a small OneNote addin which makes this possible. The source code together with the installer can be found at http://top2onenote.codeplex.com. Within this blogpost, I will explain some of the steps I took to make this addin possible.

Reading a TOP file

There is less information available about the file format that is generated by the Digital Notepad. The file extension is called .Top and it looks like it is a format invented by Waltop International Corp. When searching on the Internet for more information I found this blogpost together with some perl and python scripts to convert a Top file to SVG. I will use the logic of these scripts to convert my notes to OneNote.

Develop an OneNote AddIn

Visual Studio 2008 gives you the possiblity to develop Office Add Ins. There are different project templates supplied for Word, Excel and Outlook… but not for OneNote. There are no Visual Studio templates available for OneNote, so developing a AddIn is not that easy. Again Google gives me a nice search result, a blogpost about creating a toolbar addin for OneNote 2007: http://blogs.msdn.com/descapa/archive/2006/08/31/734298.aspx. There is a step by step tutorial supplied which will explain the different steps to develop a toolbar Addin which will popup a Hello world message. The AddIn will make use of COM Interop, which makes it possible to use C#.

The interface which should be implemented does only contains two methods. I found out that using this interface will only gives you the active OneNote page. With the OneNote.ApplicationClass (part of the Microsoft.Office.OneNote.Interop assembly) you will update the OneNote page outside OneNote. This means that the functionality of your addin does not run within OneNote, but inside a DllHost container.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using OneNote = Microsoft.Office.Interop.OneNote;

namespace Top2OneNoteAddIn
{
   [ComImport, Guid("C9590FA7-2132-47fb-9A78-AF0BF19AF4E6")]
   public interface IOneNoteAddIn
   {
      bool OnClick([In] String strActivePageID);

      bool OnEvent([In] OneNote.OneNoteAddIn_Event evt, [In] String strParameter);
   }
}

Note: together with OneNote 2007 I also installed the OneNote 2010 Beta. It seems like the AddIn functionality will change, because the addin is shown on the toolbar but clicking on it does not trigger the functionality. I think that with the release of OneNote 2010 that I will be easier to develop OneNote Addins, probably with the use of a Visual Studio template.

OpenFileDialog issue

The AddIn should, when clicked, open up a file dialog where the user can select the .top file which should be imported. Because the plugin does not run for within OneNote, the OpenFileDialog was shown below the OneNote window. I found a solution by supplying the Windows handler of OneNote as owner of the OpenFileDialog. This way the dialog is shown on top of OneNote

Process[] procs = Process.GetProcessesByName("OneNote");
IntPtr hwnd = procs[0].MainWindowHandle;

OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "top files (*.top)|*.top|All files (*.*)|*.*";
dialog.Title = "Select a top file";
dialog.ShowDialog(new WindowWrapper(hwnd));

A little WindowWrapper class is added to the solution, which implements the IWin32Window interface making it possible to supply the window handler as argument to the ShowDialog method.

public class WindowWrapper : System.Windows.Forms.IWin32Window
{
        private IntPtr _hwnd;

        public WindowWrapper(IntPtr handle)
        {
            _hwnd = handle;
        }

        #region IWin32Window Members

        public IntPtr Handle
        {
            get { return _hwnd; }
        }

        #endregion
}

InkAnalyzer

The vector data from the top file is converted to a collection of Stroke objects (for which the constructor gets a StylusPointCollection as argument). Those stoke objects are set to the InkAnalyzer. The Analyze method performs layout analysis, writing and drawing classification, and handwriting recognition. The result is given back as an AnalysisStatus object which will be used for building the XML structure used to update the OneNote page.

InkAnalyzer analyzer = new InkAnalyzer();
analyzer.AddStroke(stroke);

AnalysisStatus status = analyzer.Analyze();
if (status.Successful)
{
   Console.WriteLine(analyzer.GetRecognizedString());
}

The AddStroke method has overloaded method for which you can specify the languageId of the stroke. By default the locale settings of the active thread will be used. In my case the Dutch language id was used (because of my region settings). I use English as my OS display language. When the Dutch language pack is not installed, the result of the analyzer will not be successful. So keep in mind to check if the correct language packs are installed to make handwritten recognition possible. The method analyzer.GetInkRecognizersByPriority() gives back a list of available InkRecognizers for the different languages.

Building the OneNote XML

You can update OneNote pages by providing a XML structure. Adding or updating depends if objectids are supplied. If this is the case, the object will be updated. An example of the XML used to add the handwritten notes to OneNote

<?xml version="1.0"?>
<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2007/onenote"
        ID="{C6CCDE4D-7F47-46FA-9DD6-0A4CEC503E86}{1}{B0}">
	<one:Outline>
		<one:Position x="209.9905395507812" y="126.0"/>
		<one:Size width="245.2818908691406" height="125.3196716308594"/>
		<one:OEChildren>
			<one:OE>
				<one:InkWord recognizedText="hallo">
					<one:Data>AMIGHQSQBOYCAYABKgAaH4tCcEWEdYC+0EXZBFjPVIrml8VPjwb4utLhmyK/
7HHMzCKxQYkOuxMs7nqh3rsTi+Ves0icm6ElWrdREvqHlTLy7BdBuMAWv/Mr9HrEZm2nDbJYSZodTkA0BmwQRVOAL
HoY1USZZUo0BjFbMxSVQJ6SzJhLtWqQHBLFWlUDC0gURSNGI1cNAAAABQcLZU9mZ2hpGRQyCACAHgIh4uJBMwgA
4BICSvPiQRGrqtNBZAMVRgBAShs20YdmzeA0YdmxgA6YdmsBow7GLAB0w7Nm8AAATxMEawMVRgBAahA+gEXPAA
ARXQADggAAIqIAAAqKAW+C/gdb+B19kqxUublEssCwCSiblSgFlABLLFSyypYWLLFlliiC/gR7+BH7ACTZc3LJqblks2W
CzZQ2VNliyklZSMkzVmrKsVKSgNyygAoAESBQIxrXX3TKARMEEgARIKFVgU7sSj5OjNx+XqrpdDQFFEYAQAAABRRGA
QAAAAoAESDcOupAmua8QApiMYL+CJP4IlAAAEoAAEoAgv4BG/gEfKycSbsm5di0qry2bEGy8tUKABEg4IA52V90ygE
TBBIAESCouC8UZctFT6fx0sc+lOXuBRRGAEAAAAUURgFAAAAKP0Aj06oIUjE4DWAKWiGC/gnj+CeQBBZZQACC/gEj
+ARWM5yUt25ddnaFiwAKABEgIH932l90ygETBBIAESAf+viILJADTKojJ+vOFaLHBRRGAEAAAAUURgGAAAAJFiKA30M
Qcv/ZAAp0SYL+BFP4EVAACUALAkJRU3NypSbCwUCC/gDT+ANeJMktzPHc7Le7dtVaNy7WOIgkuJbmpZGrZQAKAB
EgYAtv1V90ygETBBIAESAUVWvkl8H9TbQgjxGuoKlABRRGAEAAAAUURgDAAAAKP0Ai63IF0ksIC+AKggFlgv4MK/gw
25SpYsFlhLAESwlsmybLNypbKAssqWUlllixZYsAgv4EY/gRkAlgZYWWFlEs2WaFUTZVkqJUuUlgAssssVAACgARIGBI
qdtfdMoBEwQSABEgk453gdolgkGItwM1/odLkQUURgBAAAAFFEYBwAAACgARIGLKJkG7kLtA
					</one:Data>
				</one:InkWord>
			</one:OE>
		</one:OEChildren>
	</one:Outline>
	<one:Outline>
		<one:Position x="225.0141754150391" y="461.2535400390625"/>
		<one:Size width="680.25830078125" height="80.7023696899414"/>
		<one:OEChildren>
			<one:OE>
				<one:InkWord recognizedText="dit">...
					</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="is">
					<one:Data>...
					</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="een">
					<one:Data>...</one:Data>
				</one:InkWord>
				<one:InkWord recognizedText="test">
					<one:Data>...
					</one:Data>
				</one:InkWord>
			</one:OE>
		</one:OEChildren>
	</one:Outline>
</one:Page>

Only the data for the first word is shown in this example to keep things clear. At the root element (Page) the ID of the existing page (provided as argument of the OnClick method) to define that the content should be inserted to the page.The InkWord data are stored as Ink Serialized Format with BASE64 encoding and placed together with the recognizedText to make searching within the handwritten notes possible.

InkWordNode iwNode = node as InkWordNode;

using (MemoryStream ms = new MemoryStream())
{
   iwNode.Strokes.Save(ms);
   byte[] isfBytes = ms.ToArray();
   dataNode.SetValue(Convert.ToBase64String(isfBytes));
}

Updating OneNote

Updating the OneNote page is done by creating an instance of the OneNote.ApplicationClass. The UpdatePageContent method is used whereby the created XML structure is supplied as argument. There is a little delay between executing the importer and when the page is updated. The cause of this delay is probally that the OneNote page is updated outside OneNote and that it will be triggered to refresh the page content.

OneNote.ApplicationClass onApplication = new OneNote.ApplicationClass();

XNamespace one = "http://schemas.microsoft.com/office/onenote/2007/onenote";
XElement root = new XElement(one + "Page", new XAttribute(XNamespace.Xmlns + "one", one));
root.Add(new XAttribute("ID", strActivePageID));

BuildXML(analyzer.RootNode, root, one);

onApplication.UpdatePageContent(root.ToString(), DateTime.MinValue);

What’s next?

The result of the importer is the same when writing the text directly with the use of a Tablet PC. Probably the results of a Tablet PC are maybe even more secure, but that’s also the reason that I don’t want to convert the handwritten notes to text.

Search within a handwritten note

Search within a handwritten note

The importer could be optimized in different ways. Some issues/ideas to improve this add in

  • Language selection – selected by import or set as a preference
  • Insert at cursor position – currently the location is set to the top left. It is possible to retrieve the cursor position by requesting the page content (GetPageContent) with the use of the OneNote.ApplicationClass. The location of the cursor is marked as a selection inside the retrieved XML structure
  • Improve text recognition – the InkAnalyzer has different properties to improve the text recognition. This way better results are achieved
  • Drag and drop support – instead of using the open file dialog, it would be nice if you can just drag the top file on your OneNote page

Like I mentioned, you can find the source at http://top2onenote.codeplex.com. It is not my goal to put a lot of effort in the development of this AddIn. If you are interested in improving this AddIn, send some feedback and I can add you as contributor to this project. If you have other questions, ask them and I will try to answer them.

XMPP and WCF .Net – Connecting a XMPP client to Openfire

With Openfire installed (http://www.johandekoning.nl/index.php/2009/11/30/xmpp-and-wcf-net-installation-of-openfire/) it is time to create some users and connect with a XMPP client to the server.

User management

Because of the simple installation, users created on the Openfire server are stored inside the embedded database. To add new users, make sure that the server runs and open the administration console (default address http://localhost:9090).

Note: I already mentioned this in my previous blog. Within my Vista environment I had to make sure that I start the Openfire server as administrator

Within the Administration Console, you will find the Users/Groups section at the top of the page. When you open this section you will get an overview of the users created on the Openfire server

User Summary

User Summary

Click on the Create New User option on the left, to create a new user. The Create User page needs some information for the new user. Provide this information and click create user to add the user. Or choose Create & Create another if you want to create another user after this user account is added.

Create User

Create User

I create two users which are called user1 and user2. I gave the the same password as their username. The overview shows the following user list.

User Summary (users created)

User Summary (users created)

One of the core extensions of the XMPP protocol is the ability to have a user list which is stored on the server (which is called a Roster). Within the Administration Console you can manage those lists, to make it possible that user1 has user2 as a contact and visa versa. This is off course also possible from within a XMPP client where you can add new users.

To manage a contacts list (roster) for a user, click on the username (of the user you want to manage) from the overview. You will get the User Properties screen, with some basic information.

User properties

User properties

At the left you will find a Roster option. When you click that option, you get an overview of the roster items. These items can be other users, or groups of users. Click the Add New Item (at the right of the screen) to add user 2 to the roster items of user 1.

Add roster item

Add roster item

When added, the user roster overview will show user 2. Perform the same action for user 2 but this time add user 1 to the roster list.

User Roster

User Roster

When both users are on both list, the user configuration part is finished. I know it is very basic, but this is jus for development. When can now use a XMPP client to make a connection with our server.

Using Psi (XMPP Client)

There are a lot of XMPP clients available on the Internet. I use Psi because it has a nice XML logging feature, to make the XMPP stream visible. I can use this sample stream for development of a XMPP client in .Net without the use of WCF. You can download the Psi client at http://psi-im.org/download/

When you start the client for the first time, it will show you a message that you need to set up an account. Click on the Use existing account option, because our test accounts are already available on the server.

Account setup

Account setup

On the first tab (Account) fill in the Jabber ID (JID) and password. The jabber ids for our users are user1@localfire and user2@localfire . The password are user1 and user2.

Account Tab

Account Tab

At the connection tab, we need to specify our server location. This is localhost when it runs on the same machine where you installed this client. For testing purposes we will not use an encrypt connection (Never).

Connection tab

Connection tab

Click save to store the account information. The main screen of Psi will be visible whereby the user status is set to Offline.

Psi main screen

Psi main screen

Before we connect to our Openfire server, I want to enable the XML console to get an idea of the stream which is send between client and server. Right click on the first item of the list (openfire) and select Open XML Console… Within the XML Console, mark the enable checbox at the bottem left. To get the client connected, select from the dropdownlist at the bottom of the main screen, the Online status. The client will connect to the server and the stream will be made visible in the XML Console dialog.

XML Console

XML Console

The XMPP stream

I connected another client to the server as user 2. Between both users i send a message with the text “Hello” (User 1 -> User 2) and “Goodbye” (User 2 -> User 1). The presence of user 1 will be set to Offline and the application will be closed. Below you find the captured XMPP stream.

What’s next?

This blog post explained how to set up the connecting between a XMPP client and server. The captured stream will be used for further analysis. The next step will be the development of a simple XMPP client which will echo back received messages in reverse order. This client will not be written with Windows Communication Foundation to get some feeling how a .Net client with the TcpClient class will work.

If you have questions after reading this post, or have other feedback? Please send in a comment.

<!--client-->
<?xml version="1.0"?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="localfire" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" >

<!--server-->
<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="localfire" id="c035b19c" xml:lang="en" version="1.0">

<stream:features>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism>
<mechanism>ANONYMOUS</mechanism>
<mechanism>CRAM-MD5</mechanism>
</mechanisms>
<compression xmlns="http://jabber.org/features/compress">
<method>zlib</method>
</compression>
<auth xmlns="http://jabber.org/features/iq-auth"/>
<register xmlns="http://jabber.org/features/iq-register"/>
</stream:features>

<!--client-->
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5" />

<!--server-->
<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImxvY2FsZmlyZSIsbm9uY2U9IitaQVRkeUZNeWJjV1ZveHJHTXNIV2dxNVR1QWhNUkFoQ08wVWlp
VzciLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=</challenge>

<!--client-->
<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dXNlcm5hbWU9InVzZXIxIixyZWFsbT0ibG9jYWxmaXJlIixub25jZT0iK1pBVGR5Rk15YmNXVm94ckdNc0hXZ3E1
VHVBaE1SQWhDTzBVaWlXNyIsY25vbmNlPSJhM0dMY21ra3VFUjEwaVBTMlJzQnpRZWlBY2pzSnIzNVNyTzRkTFNzM
2FnPSIsbmM9MDAwMDAwMDEsZGlnZXN0LXVyaT0ieG1wcC9sb2NhbGZpcmUiLHFvcD1hdXRoLHJlc3BvbnNlPWM1N
TlhMjBhMmFkZGVjYmJjOTFkZTZkNDYwODYyODU5LGNoYXJzZXQ9dXRmLTg=</response>

<!--server-->
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD04NjMxMTIzNDZlM2MxOTUxMGZlNzFmNTA5N2E0MWViMg==</success>

<!--client-->
<?xml version="1.0"?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="localfire" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" >

<!--server-->
<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="localfire" id="c035b19c" xml:lang="en" version="1.0">
<stream:features>
<compression xmlns="http://jabber.org/features/compress">
<method>zlib</method>
</compression>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</stream:features>

<!--client-->
<iq type="set" id="bind_1" >
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<resource>Home</resource>
</bind>
</iq>

<!--server-->
<iq xmlns="jabber:client" type="result" id="bind_1" to="localfire/c035b19c" >
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<jid>user1@localfire/Home</jid>
</bind>
</iq>

<!--client-->
<iq type="set" id="aab7a" >
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</iq>

<!--server-->
<iq type="result" id="aab7a" to="user1@localfire/Home" >
<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</iq>

<!--client-->
<iq type="get" id="aab8a" >
<query xmlns="jabber:iq:roster"/>
</iq>

<!--server-->
<iq type="result" id="aab8a" to="user1@localfire/Home" >
<query xmlns="jabber:iq:roster">
<item subscription="both" jid="user2@localfire" />
</query>
</iq>

<!--client-->
<presence>
<priority>5</priority>
<c xmlns="http://jabber.org/protocol/caps" node="http://psi-im.org/caps" ver="0.13-dev-rev2" ext="ca cs ep-notify html" />
</presence>

<iq type="get" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" />
</iq>

<iq type="get" id="aabba" >
<query xmlns="jabber:iq:private">
<storage xmlns="storage:bookmarks"/>
</query>
</iq>

<iq type="get" to="user1@localfire" id="aabca" >
<vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN" />
</iq>

<iq type="get" to="localfire" id="aabda" >
<query xmlns="http://jabber.org/protocol/disco#info"/>
</iq>

<!--server-->
<presence from="user2@localfire/Work" to="user1@localfire/Home" >
<c xmlns="http://jabber.org/protocol/caps" node="http://exodus.jabberstudio.org/caps" ext="xhtml-im" ver="0.10.0.0" />
<priority>1</priority>
</presence>

<!--client-->
<iq type="get" to="user2@localfire/Work" id="aabea" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#0.10.0.0" />
</iq>

<iq type="get" to="user2@localfire/Work" id="aabfa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#xhtml-im" />
</iq>

<!--server-->
<iq from="user1@localfire/Home" type="get" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" />
</iq>

<!--client-->
<iq type="result" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" >
<identity category="client" type="pc" name="Psi" />
<feature var="urn:xmpp:jingle:1" />
<feature var="urn:xmpp:jingle:transports:ice-udp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:audio" />
</query>
</iq>

<!--server-->
<iq type="result" id="aabba" to="user1@localfire/Home" >
<query xmlns="jabber:iq:private">

<storage xmlns="storage:bookmarks"/>
</query>
</iq>

<iq from="user1@localfire" type="result" id="aabca" to="user1@localfire/Home" >
<vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN" >
<FN>User 1</FN>
</vCard>
</iq>

<iq from="localfire" type="result" id="aabda" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="server" type="im" name="Openfire Server" />
<identity category="pubsub" type="pep" />
<feature var="http://jabber.org/protocol/pubsub#manage-subscriptions" />
<feature var="http://jabber.org/protocol/pubsub#modify-affiliations" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-default" />
<feature var="http://jabber.org/protocol/pubsub#collections" />
<feature var="jabber:iq:private" />
<feature var="http://jabber.org/protocol/disco#items" />
<feature var="vcard-temp" />
<feature var="http://jabber.org/protocol/pubsub#publish" />
<feature var="http://jabber.org/protocol/pubsub#subscribe" />
<feature var="http://jabber.org/protocol/pubsub#retract-items" />
<feature var="http://jabber.org/protocol/offline" />
<feature var="http://jabber.org/protocol/pubsub#meta-data" />
<feature var="jabber:iq:register" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions" />
<feature var="http://jabber.org/protocol/pubsub#default_access_model_open" />
<feature var="jabber:iq:roster" />
<feature var="http://jabber.org/protocol/pubsub#config-node" />
<feature var="http://jabber.org/protocol/address" />
<feature var="http://jabber.org/protocol/pubsub#publisher-affiliation" />
<feature var="http://jabber.org/protocol/pubsub#item-ids" />
<feature var="http://jabber.org/protocol/pubsub#instant-nodes" />
<feature var="http://jabber.org/protocol/commands" />
<feature var="http://jabber.org/protocol/pubsub#multi-subscribe" />
<feature var="http://jabber.org/protocol/pubsub#outcast-affiliation" />
<feature var="http://jabber.org/protocol/pubsub#get-pending" />
<feature var="google:jingleinfo" />
<feature var="jabber:iq:privacy" />
<feature var="http://jabber.org/protocol/pubsub#subscription-options" />
<feature var="jabber:iq:last" />
<feature var="http://jabber.org/protocol/pubsub#create-and-configure" />
<feature var="urn:xmpp:ping" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-items" />
<feature var="jabber:iq:time" />
<feature var="http://jabber.org/protocol/pubsub#create-nodes" />
<feature var="http://jabber.org/protocol/pubsub#persistent-items" />
<feature var="jabber:iq:version" />
<feature var="http://jabber.org/protocol/pubsub#presence-notifications" />
<feature var="http://jabber.org/protocol/pubsub" />
<feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations" />
<feature var="http://jabber.org/protocol/pubsub#delete-nodes" />
<feature var="http://jabber.org/protocol/pubsub#purge-nodes" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="http://jabber.org/protocol/rsm" />
</query>
</iq>

<iq from="user2@localfire/Work" type="result" id="aabea" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#0.10.0.0" >
<feature var="jabber:iq:agents" />
<feature var="jabber:iq:oob" />
<feature var="jabber:iq:browse" />
<feature var="jabber:iq:time" />
<feature var="jabber:iq:version" />
<feature var="jabber:iq:last" />
<feature var="http://jabber.org/protocol/disco#items" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="storage:bookmarks" />
<feature var="jabber:x:data" />
<feature var="jabber:x:conference" />
<feature var="jabber:x:event" />
<feature var="http://jabber.org/protocol/muc" />
<feature var="http://jabber.org/protocol/muc#user" />
<feature var="http://jabber.org/protocol/muc#owner" />
<feature var="http://jabber.org/protocol/si" />
<feature var="http://jabber.org/protocol/si/profile/file-transfer" />
<feature var="http://jabber.org/protocol/bytestreams" />
</query>
</iq>

<iq from="user2@localfire/Work" type="result" id="aabfa" to="user1@localfire/Home" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://exodus.jabberstudio.org/caps#xhtml-im" >
<feature var="http://jabber.org/protocol/xhtml-im" />
</query>
</iq>

<iq from="user1@localfire/Home" type="result" to="user1@localfire/Home" id="aabaa" >
<query xmlns="http://jabber.org/protocol/disco#info" node="http://psi-im.org/caps#ca" >
<identity category="client" type="pc" name="Psi" />
<feature var="urn:xmpp:jingle:1" />
<feature var="urn:xmpp:jingle:transports:ice-udp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:1" />
<feature var="urn:xmpp:jingle:apps:rtp:audio" />
</query>
</iq>

<!--client-->
<message type="chat" to="user2@localfire" id="aac0a" >
<body>Hello</body>
<active xmlns="http://jabber.org/protocol/chatstates"/>
</message>

<!--server-->
<message from="user2@localfire/Work" type="chat" id="jcl_13" to="user1@localfire/Home" >
<body>Goodbye</body>
<x xmlns="jabber:x:event">
<composing/>
</x>
<html xmlns="http://jabber.org/protocol/xhtml-im">
<body xmlns="http://www.w3.org/1999/xhtml">
<p style="font-size:small;font-family:Arial;color:#000000" >Goodbye</p>
</body>
</html>
</message>

<!--client-->
<presence type="unavailable" >
<status>Logged out</status>
</presence>

</stream:stream>

<!--server-->
</stream:stream>