Monday, March 8, 2010

RxSandbox V1

[New version (2011.05.06)]

[RxSandbox downlaod has been upgraded to the newest version of Rx (Build 1.0.2677.0 08/27/2010).] download

[RxSandbox downlaod has been upgraded to the newest version of Rx (Build 1.0.2617.0 07/15/2010).] download
(Changes: solution converted to VS2010, Net 4.0; 56 operators, grouping operators on the tree control; zoom)

[RxSandbox downlaod has been upgraded to the newest version of Rx (Build 1.0.2441.0 04/14/2010).] download

[RxSandbox downlaod has been upgraded to the newest version of Rx (Build 1.0.2350.0 03/15/2010).] download

I am really happy to announce that the new version of RxSandbox has been just released. Last time I have been writing about the concept and the main ideas behind the project. That was just a small prototype, the proof of concept. This release is much more mature, the user can test more standard Rx operators and the API has been changed a little bit, however the concept stated the same.

These are the main new features:

  • Marble diagrams
  • New powerful API
  • Extensibility mechanism
  • Description of Rx the operators taken from Rx documentation

Lets start from the the end-user which is not necessary interested in writing any code. He just wants to experiment with Rx operators, check how they behave in specific scenarios. When we start RxSandbox application we will see the list of standard operators on the left hand side. Lets assume that we don’t know how Merge operator works. After double click on the Merge node the new tab will be displayed. We can find the short description of Merge operator taken from documentation provided by Rx installer, we can also find the code sample that can be tested interactively trough UI. Each input argument is presented as a simple automatically generated UI with one textbox where we can write input value, tree buttons and the history of the source. The output of the expression can be presented in two ways: by a simple list displaying the results (“Output” tab) or the marble diagrams drawn live during testing (‘Marble diagram – Live” tab). There is also one tab called “Marble diagram - Definition” showing the operator definition.

image

Now lets see what the developer can do with RxSandbox. The most important class in Rx API is the ExpressionDefinition. It holds all necessary information describing tested expression such as name, description and sample code.

image

When we look inside RxSandbox project we will see that the definitions of all standard operators are very similar, for example the Merge operator is defined like this:

public static ExpressionDefinition Merge()
{
    Expression<Func<IObservable<string>, IObservable<string>, IObservable<string>,
        IObservable<string>>> expression
            = (a, b, c) => Observable.Merge(a, b, c);
    
    return ExpressionDefinition.Create(expression);
}

This is all we need to write. Other things like operator’s name, description and the text of expression can be inferred from the Linq expression. Of course all these information can be set manually using appropriate Create method overload and ExpressionSettings class. All .Net types are supported as observable type, not only the System.String type like in this example. The only requirement is there must exist a TypeConverter for that type. Later in this post I’ll how implement TypeConverter for custom type and how to create ExpressionDefinition without using Linq expression but using the whole method with many statements. The second very important class in RxSandbox API is an ExpressionInstance class which is very useful in scenarios where we want to use some RxSandbox functionalities directly from code without any UI experience (for example during writing Unit Tests or recording marble diagram).

Expression<Func<IObservable<string>, IObservable<string>, IObservable<string>,
   IObservable<string>>> expression
       = (a, b, c) => Observable.Merge(a, b, c);

ExpressionDefinition definition = ExpressionDefinition.Create(expression);

using (var instance = ExpressionInstance.Create(definition))
{
    ExpressionInstance instance = ExpressionInstance.Create(definition);

    // using non-generic type 'ObservableSource'
    ObservableSource output1 = instance.Output;
    output1.ObservableStr.Subscribe(Console.WriteLine);

    // using generic type 'ObservableSource<T>'
    ObservableOutput<string> output2 = instance.Output as ObservableOutput<string>;
    output2.Observable.Subscribe(Console.WriteLine);

    instance["a"].OnNext("one");    // using non-generic type 'ObservableInput'
    (instance["a"] as ObservableInput<string>).OnNext("two"); // using generic type
    instance["b"].OnNext("tree");
    instance["a"].OnCompleted();
    instance["b"].OnCompleted();
    instance["c"].OnCompleted();
}

When we add delay before sending each input signal we can very easily record sample marble diagram.

internal static class Extensions
{
    internal static void OnNext2(this ObservableInput input, string value)
    {
        input.OnNext(value);
        Thread.Sleep(100);
    }
    internal static void OnError2(this ObservableInput input)
    {
        input.OnError(new Exception());
        Thread.Sleep(100);
    }
    internal static void OnCompleted2(this ObservableInput input)
    {
        input.OnCompleted();
        Thread.Sleep(100);
    }
}

Marble diagrams are described in a very simple object model.

image

This object model can be serialized to Xml format, for instance the code above creates fallowing marble diagram:

<Diagram>
  <Input Name="a">
    <Marble Value="one" Order="0" />
    <Marble Value="two" Order="1" />
    <Marble Kind="OnCompleted" Order="3" />
  </Input>
  <Input Name="b">
    <Marble Value="tree" Order="2" />
    <Marble Kind="OnCompleted" Order="4" />
  </Input>
  <Input Name="c">
    <Marble Kind="OnCompleted" Order="5" />
  </Input>
  <Output>
    <Marble Value="one" Order="0" />
    <Marble Value="two" Order="1" />
    <Marble Value="tree" Order="2" />
    <Marble Kind="OnCompleted" Order="5" />
  </Output>
</Diagram>

image

As we can see single marble diagram have a very simple Xml representation and diagrams for all standard operators from RxSandbox are stored in Diagrams.xml file (this file path can be changed in the configuration files).

Short description added to all standard operators extracted from Rx documentation Xml file is a next new feature of current release. Of course not all tested reactive expression are related to one particular operator so the description can be set manually (ExpressionSettings.Description property). When we want write a very complicated Linq expression or the expression is passed as delegate type to the ExpressionDefinition.Create method and we also want to provide description from particular Rx operator at the same time, we can do it indicating operator thought MethodInfo type (ExpressionSettings.Operator property).

The last but not least new feature is the extensibility mechanism. When we want to write our custom reactive expression without changing anything inside RxSadbox project we can do it by implementing IExpressionProvider interface directly or by inheriting from an abstract class ExpressionAttributeBasedProvider and setting our assembly name in the configuration file (ExtensionsAssembly element). During startup process RxSandbox loads that assembly, analyzes it and finds all expression providers. Few weeks ago I have been writing about Incremental Find implemented using Rx, lets see how such a query can tested via RxSandbox.

[AttributeUsage(AttributeTargets.Method,AllowMultiple = false, Inherited = true)]
public class ExpressionAttribute : Attribute { }

public interface IExpressionProvider
{
    IEnumerable<ExpressionDefinition> GetExpressions();
}

public abstract class ExpressionAttributeBasedProvider : IExpressionProvider
{
    public IEnumerable<ExpressionDefinition> GetExpressions()
    {
        var q =
            from m in this.GetType().GetMethods()
            let attr = Attribute.GetCustomAttribute(m, typeof(ExpressionAttribute)) 
                as ExpressionAttribute
            where attr != null
            select m.Invoke(null, null) as ExpressionDefinition;
        return q.ToList();
    }
}

public class CustomExpressions : ExpressionAttributeBasedProvider
{
    [Expression]
    public static ExpressionDefinition IncrementalSearch()
    {
        Func<IObservable<string>, IObservable<Person>, IObservable<Person>> expression
                = (codeChanged, webServiceCall) =>
                      {
                        var q =
                            from code in codeChanged
                            from x in Observable.Return(new Unit())
                                .Delay(TimeSpan.FromSeconds(4)).TakeUntil(codeChanged)
                            from result in webServiceCall.TakeUntil(codeChanged)
                            select result;

                          return q;
                      };

        return ExpressionDefinition.Create(expression, new ExpressionSettings
           {
               Name = "Incremental find",
               Description = @"Send the code of the person you are looking for, "
                    + "after four seconds (if you don't send new code again) web service "
                    + "will be called. The result won't be returned if new code is provided "
                    + "in the meantime.",                   
           });
    }
}

[TypeConverter(typeof(PersonConverter))]
public class Person
{
    public string Code { get; set; }
    public string Name { get; set; }
}

public class PersonConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof (string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }
    
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value is string)
        {
            string[] v = ((string)value).Split(new[] { ',' });
            return new Person {Code = v[0], Name = v[1]};
        }
        return base.ConvertFrom(context, culture, value);
    }
    
    public override object ConvertTo(ITypeDescriptorContext context,
       CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof (string))
        {
            var person = value as Person;
            return person.Code + "," + person.Name;
        }                
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

So that’s it for this release of RxSandbox. I encourage you to play with it a little bit and let me know what you think.

And that’s not the end of the project. In upcoming releases I plan to:

  • create Silverlight version with ability to write reactive expression directly in the web browser
  • add integration with MEF
  • add better look and feel experience

Here you can download sources and binaries

11 comments:

Unknown said...

I cannot run the application, it errors with 'Exception has been thrown by the target of an invocation'

Marcin Najder said...

Probably the problem is that RxSandbox.exe cannot load Rx dlls. Please download once again binaries, you should see all necessary dlls next to the RxSandbox.exe file or install the newest version of Rx.

Sorry and thanks for letting me know. I hope it works.

Unknown said...

Just downloaded Post015 RxSandbox V1.zip again and got the same problem.

Here are the list of files in the bin folder
Diagrams.xml
RxSandbox.exe
RxSandbox.exe.config
RxSandbox.pdb
RxSandbox.vshost.exe
RxSandbox.vshost.exe.config
System.CoreEx.dll
System.CoreEx.xml
System.Interactive.dll
System.Interactive.xml
System.Observable.dll
System.Reactive.dll
System.Reactive.xml
System.Threading.dll

Please note I do not have .Net 4.0 installed.

I've installed the Rx for 3.50 SP1.

Thanks

Marcin Najder said...

I think I have found the problem. Probably you have installed the wrong version of Rx .Net 3.5 Sp1. The latest version of Rx installer is v1.0.2317.0 (the previous one is v1.0.2149.0).

Bin folder contains all necessary dlls but the problem is that the version of Rx dlls has not been changed with the new release of Rx :| and somehow wrong Rx dlls are being loaded.

Try to uninstall Rx .Net3.5 Sp1 version and run RxSandbox application. If everything works fine just install the newest version of Rx. I hope this helps.

Thanks

Unknown said...

Thanks that worked.

I had v1.0.2149.0 installed.

Where is the installation for v1.0.2317.0?

I went to the link http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx

and the button Rx for .NET Framework 3.5 SP1 is still v1.0.2149.0

Unknown said...

Sorry its not. its v1.0.2317.

Thanks for your help.

Marcin Najder said...

No problem :)

Please notice:

[RxSandbox downlaod has been upgraded to the newest version of Rx (Build 1.0.2350.0 03/15/2010).]

Anonymous said...

This tool is amazing! Fantastic work!

A few of suggestions:
1. Have an option to make the balls in the diagram a bit larger and put the text inside them rather than the tooltip, that way you can understand how a method works in one glance without moving the mouse to read all the tooltips.

2. Turn the input textbox into an editable combobox, with the combobox menu option "Current Time". If "Current Time" is selected then clicking on OnNext should use the current time "mm:ss.millisec" format. This will help understanding of time-based operations. Another option is to just add an option to view the results on a time scale.

Josh Elster (Liquid Electron) said...

Great tool, looks very promising. I came across it when I was looking at creating a template for authoring marble diagrams (not necessarily with code), but I decided to play around with the source a bit anyway, since it was so neat.

Long story short, I created a StringExpressionProvider implementation that uses Roslyn to compile text snippets into expressions that can then be displayed on the marble diagram. That parts works oh so beautifully, but I'm having a bit of a snag in the WPF rendering code, specifically with the Series. Are you accepting patch submissions for this project? I'd love to share!

Marcin Najder said...

hi,
of course, just let me know what I could fix :) or, what do you think about publishing the source code on the codeplex so you could change anything you want yourself ?

Josh Elster (Liquid Electron) said...

I wasn't able to get the ui to show the static diagram, but that wasnt really the full focus. I blogged about the changes that I made here - your code is extensible enough that a single page's worth of code is enough to get started. Something worth examining as a rendering option I think would be using a chart of some kind, or perhaps just refactoring the way marbles are being created and drawn - instead of a grid, perhaps a canvas for positioning? The margin between two marbles is some constant factor of the time interval between events.