The Problem Solver

Tell me and I will forget
Show me and I will remember
Involve me and I will understand
- Confucius -

Google Ads

This Blog

Syndication

Search

Tags

News





  • View Maurice De Beijer's profile on LinkedIn

Community

Email Notifications

Explore

Archives

April 2010 - Posts

With WF4 it is quite easy to use data that is part of the request message, for example an order identifier, to route multiple WCF messages to the same workflow. It is also possible to have multiple messages that can start a new workflow. In this blog post I am going to show both these concepts in a single workflow. This workflow is going to be able to receive three different messages, AddItem, AddExpensiveItem and Submit.  Either AddItem or AddExpensiveItem can start a new workflow while Submit can only be done with an existing workflow.

The final workflow looks like this±

image

 

To create the workflow you need to start by creating a new WCF Workflow Service Application in Visual Studio 2010. Next delete the default activities that have been created by the template and replace it with a Sequence activity. Add a variable named TotalAmount of type Int32 and a Boolean variable named OrderSubmitted. Add a DoWhile activity and set its condition to OrderSubmitted = false so it will keep on executing until we set the OrderSubmitted variable to true.

So much for the boring part, now we get to the real message part.

First we need to add a Pick activity. This is needed because we want to receive multiple different messages without specifying an order. Add a third PickBrach so we get a total of three branches and a a Receive activity to each Trigger. Set all three ServiceContractName’s to OrderService and set the OperationName to AddItem, AddExpensiveItem and Submit respectively. Now the AddItem and AddExpensiveItem requests are allowed to start a new workflow so check the CanCreateInstance for both of them.

Next the important part for the correlation bit. All three requests need to share some common piece of data.Below is the Content definition of the AddItem request.

image

Both the AddExpensiveItem and Submit requests only contain the orderId parameter

image

The important is that they all share the same orderId that the client needs to provide.

 

That takes care of sending some common data but we still need to configure the workflow to use that to correlate requests to the same workflow. This last part is done using the CorrelatesOn property of the Receive activity. In all three point this to the orderId being passed in and make sure all three have a matching key.

image

 

After we have added the processing part to the Action part of each PickBranch we are all set to go.

Note: to create the SendReply, just right click the Receive activity and select Create SendReply. This will be placed on the clipboard so now you can paste it into the Action Sequence.

 

Running the workflow

Press F5 to activate the WCF Test Client. You should see three possible actions, either AddItem or AddExpensiveItem can start a workflow or send messages to an existing workflow. It just depends on whether the orderId passed in can be correlated to an existing workflow. If so that workflow receives the message, otherwise a new workflow is started.

image

The Submit message on the other hand can only be executed with an existing workflow.

 

 

Just in case anyone is interested below is the complete XAML for the workflow:

<WorkflowService mc:Ignorable="sap" ConfigurationName="Service1" sap:VirtualizedContainerService.HintSize="1193,850" Name="Service1" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/servicemodel" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:ssa="clr-namespace:System.ServiceModel.Activities;assembly=System.ServiceModel.Activities" xmlns:ssx="clr-namespace:System.ServiceModel.XamlIntegration;assembly=System.ServiceModel" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <p:Sequence sad:XamlDebuggerXmlReader.FileName="c:\users\maurice\documents\visual studio 2010\Projects\DeclarativeServiceLibrary1\DeclarativeServiceLibrary1\Service1.xamlx" sap:VirtualizedContainerService.HintSize="1163,820" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces">
    <p:Sequence.Variables>
      <p:Variable x:TypeArguments="x:Int32" Name="TotalAmount" />
    </p:Sequence.Variables>
    <sap:WorkflowViewStateService.ViewState>
      <scg3:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg3:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <p:DoWhile sap:VirtualizedContainerService.HintSize="1141,696">
      <p:DoWhile.Variables>
        <p:Variable x:TypeArguments="x:Boolean" Name="OrderSubmitted" />
      </p:DoWhile.Variables>
      <p:DoWhile.Condition>[OrderSubmitted = False]</p:DoWhile.Condition>
      <p:Pick sap:VirtualizedContainerService.HintSize="1115,576">
        <p:PickBranch DisplayName="Branch1" sap:VirtualizedContainerService.HintSize="307,530">
          <p:PickBranch.Variables>
            <p:Variable x:TypeArguments="CorrelationHandle" Name="__handle1" />
            <p:Variable x:TypeArguments="x:Int32" Name="Price" />
          </p:PickBranch.Variables>
          <p:PickBranch.Trigger>
            <x:Reference>__ReferenceID0</x:Reference>
          </p:PickBranch.Trigger>
          <p:Sequence sap:VirtualizedContainerService.HintSize="277,312">
            <sap:WorkflowViewStateService.ViewState>
              <scg3:Dictionary x:TypeArguments="x:String, x:Object">
                <x:Boolean x:Key="IsExpanded">True</x:Boolean>
              </scg3:Dictionary>
            </sap:WorkflowViewStateService.ViewState>
            <p:Assign sap:VirtualizedContainerService.HintSize="255,58">
              <p:Assign.To>
                <p:OutArgument x:TypeArguments="x:Int32">[TotalAmount]</p:OutArgument>
              </p:Assign.To>
              <p:Assign.Value>
                <p:InArgument x:TypeArguments="x:Int32">[TotalAmount + Price]</p:InArgument>
              </p:Assign.Value>
            </p:Assign>
            <SendReply DisplayName="SendReplyToReceive" sap:VirtualizedContainerService.HintSize="255,90">
              <SendReply.Request>
                <Receive x:Name="__ReferenceID0" CanCreateInstance="True" sap:VirtualizedContainerService.HintSize="277,100" OperationName="AddItem" ServiceContractName="OrderService">
                  <Receive.CorrelatesOn>
                    <XPathMessageQuery x:Key="key1">
                      <XPathMessageQuery.Namespaces>
                        <ssx:XPathMessageContextMarkup>
                          <x:String x:Key="xgSc">http://tempuri.org/</x:String>
                        </ssx:XPathMessageContextMarkup>
                      </XPathMessageQuery.Namespaces>sm:body()/xgSc:AddItem/xgSc:orderId</XPathMessageQuery>
                  </Receive.CorrelatesOn>
                  <Receive.CorrelationInitializers>
                    <RequestReplyCorrelationInitializer CorrelationHandle="[__handle1]" />
                  </Receive.CorrelationInitializers>
                  <ReceiveParametersContent>
                    <p:OutArgument x:TypeArguments="x:Int32" x:Key="orderId" />
                    <p:OutArgument x:TypeArguments="x:String" x:Key="item" />
                    <p:OutArgument x:TypeArguments="x:Int32" x:Key="price">[Price]</p:OutArgument>
                  </ReceiveParametersContent>
                </Receive>
              </SendReply.Request>
              <SendParametersContent>
                <p:InArgument x:TypeArguments="x:Int32" x:Key="Total">[TotalAmount]</p:InArgument>
              </SendParametersContent>
            </SendReply>
          </p:Sequence>
        </p:PickBranch>
        <p:PickBranch sap:VirtualizedContainerService.HintSize="307,530">
          <p:PickBranch.Variables>
            <p:Variable x:TypeArguments="CorrelationHandle" Name="__handle1" />
          </p:PickBranch.Variables>
          <p:PickBranch.Trigger>
            <x:Reference>__ReferenceID1</x:Reference>
          </p:PickBranch.Trigger>
          <p:Sequence sap:VirtualizedContainerService.HintSize="277,312">
            <sap:WorkflowViewStateService.ViewState>
              <scg3:Dictionary x:TypeArguments="x:String, x:Object">
                <x:Boolean x:Key="IsExpanded">True</x:Boolean>
              </scg3:Dictionary>
            </sap:WorkflowViewStateService.ViewState>
            <p:Assign sap:VirtualizedContainerService.HintSize="255,58">
              <p:Assign.To>
                <p:OutArgument x:TypeArguments="x:Int32">[TotalAmount]</p:OutArgument>
              </p:Assign.To>
              <p:Assign.Value>
                <p:InArgument x:TypeArguments="x:Int32">[TotalAmount + 100]</p:InArgument>
              </p:Assign.Value>
            </p:Assign>
            <SendReply DisplayName="SendReplyToReceive" sap:VirtualizedContainerService.HintSize="255,90">
              <SendReply.Request>
                <Receive x:Name="__ReferenceID1" CanCreateInstance="True" sap:VirtualizedContainerService.HintSize="277,100" OperationName="AddExpensiveItem" ServiceContractName="OrderService">
                  <Receive.CorrelatesOn>
                    <XPathMessageQuery x:Key="key1">
                      <XPathMessageQuery.Namespaces>
                        <ssx:XPathMessageContextMarkup>
                          <x:String x:Key="xgSc">http://tempuri.org/</x:String>
                        </ssx:XPathMessageContextMarkup>
                      </XPathMessageQuery.Namespaces>sm:body()/xgSc:AddExpensiveItem/xgSc:orderId</XPathMessageQuery>
                  </Receive.CorrelatesOn>
                  <Receive.CorrelationInitializers>
                    <RequestReplyCorrelationInitializer CorrelationHandle="[__handle1]" />
                  </Receive.CorrelationInitializers>
                  <ReceiveParametersContent>
                    <p:OutArgument x:TypeArguments="x:Int32" x:Key="orderId" />
                  </ReceiveParametersContent>
                </Receive>
              </SendReply.Request>
              <SendParametersContent>
                <p:InArgument x:TypeArguments="x:Int32" x:Key="Total">[TotalAmount]</p:InArgument>
              </SendParametersContent>
            </SendReply>
          </p:Sequence>
        </p:PickBranch>
        <p:PickBranch DisplayName="Branch2" sap:VirtualizedContainerService.HintSize="307,530">
          <p:PickBranch.Variables>
            <p:Variable x:TypeArguments="CorrelationHandle" Name="__handle2" />
          </p:PickBranch.Variables>
          <p:PickBranch.Trigger>
            <x:Reference>__ReferenceID2</x:Reference>
          </p:PickBranch.Trigger>
          <p:Sequence sap:VirtualizedContainerService.HintSize="277,312">
            <sap:WorkflowViewStateService.ViewState>
              <scg3:Dictionary x:TypeArguments="x:String, x:Object">
                <x:Boolean x:Key="IsExpanded">True</x:Boolean>
              </scg3:Dictionary>
            </sap:WorkflowViewStateService.ViewState>
            <SendReply DisplayName="SendReplyToReceive" sap:VirtualizedContainerService.HintSize="255,90">
              <SendReply.Request>
                <Receive x:Name="__ReferenceID2" sap:VirtualizedContainerService.HintSize="277,100" OperationName="Submit" ServiceContractName="OrderService">
                  <Receive.CorrelatesOn>
                    <XPathMessageQuery x:Key="key1">
                      <XPathMessageQuery.Namespaces>
                        <ssx:XPathMessageContextMarkup>
                          <x:String x:Key="xgSc">http://tempuri.org/</x:String>
                        </ssx:XPathMessageContextMarkup>
                      </XPathMessageQuery.Namespaces>sm:body()/xgSc:Submit/xgSc:orderId</XPathMessageQuery>
                  </Receive.CorrelatesOn>
                  <Receive.CorrelationInitializers>
                    <RequestReplyCorrelationInitializer CorrelationHandle="[__handle2]" />
                  </Receive.CorrelationInitializers>
                  <ReceiveParametersContent>
                    <p:OutArgument x:TypeArguments="x:Int32" x:Key="orderId" />
                  </ReceiveParametersContent>
                </Receive>
              </SendReply.Request>
            </SendReply>
            <p:Assign sap:VirtualizedContainerService.HintSize="255,58">
              <p:Assign.To>
                <p:OutArgument x:TypeArguments="x:Boolean">[OrderSubmitted]</p:OutArgument>
              </p:Assign.To>
              <p:Assign.Value>
                <p:InArgument x:TypeArguments="x:Boolean">True</p:InArgument>
              </p:Assign.Value>
            </p:Assign>
          </p:Sequence>
        </p:PickBranch>
      </p:Pick>
    </p:DoWhile>
  </p:Sequence>
</WorkflowService>

 

Enjoy!

 

www.TheProblemSolver.nl
Wiki.WindowsWorkflowFoundation.eu

Posted by Maurice | 6 comment(s)
Filed under: , , , ,

Recently the workflow team at Microsoft released a first CTP to the ADO.NET Activity Pack. In this blog post I am going to take a quick look at how to get started with these activities.

 

First step is installing the activity pack. Download the setup MSI from http://wf.codeplex.com/releases/view/43585 and run the installer. This will install the activities in the folder C:\Program Files (x86)\Microsoft WF ADO.NET Activity Pack\CTP 1 and register them with VS2010 so they automatically appear in the toolbox. Note that the main assembly containing the activities is called Microsoft.Data.Activities.dll.

If we start VS2010 and create a new workflow project the 3 ADO.NET activities are right there in the toolbox.

image

 

All three activities operate in a similar manner so I am going to use the ExecuteSqlQuery<T> to demonstrate how to use these activities. The sample is simple, all I am going to do is load a set of customers from the Northwind database and display these on the screen.

First step is to define a customer type the workflow can work with. This is as simple as they come:

public class Customer
{
    public string CustomerId { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
 
}

Next you can drag a ExecuteSqlQuery<T> onto the design surface. I put them inside of a Sequence so I can print the result after the ExecuteSqlQuery<T> is finished. When prompted for the type select Brose for Types … and select the Customer from the current project.

image

 

Now the activity is on the design surface but as can be seen from the Error List both the connection and the command need to be specified.

 

First the connection because that is a bit misleading, at least for me the first time I tried.

Clicking the connection button in the property sheet allows you to use an existing database connection or create a new one. Nice but that does mean your connection string will be part of the workflow, not so good, and is a literal instead of an expression so not easy to change. The proper way to do this is to add a connection string setting to the project first. And once that is done this connection will also appear in the Configure Database Connection dialog for the ExecuteSqlQuery<T>,

image

Next step is to specify the command to execute.

image

Here we are doing a simple select from the customers table and filtering the results to only include the UK based companies. All we have is a simple textbox to enter a SQL command, table or stored procedure name. Quite primitive [:(]. Keep in mind that the select * is quite a bad idea as well and you are far better of specifying the field names as we will see in a bit.

image

Next we need to map the data loaded to our customer type. For this purpose we get a record variable that points to a SqlDataReader and we can use the record.GetString(index) and similar functions to load the data. This requires passing an index, the reason doing a SELECT * is not a smart mover

 

image

When this is done all we need to do is capture the Result property, which contains a List<Customer> and loop over this printing the content.

image

 

The complete workflow in XAML:

<Activity mc:Ignorable="sap" x:Class="WorkflowConsoleApplication7.Workflow1" sap:VirtualizedContainerService.HintSize="356,1058" mva:VisualBasic.Settings="Assembly references and imported namespaces for internal implementation" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:local="clr-namespace:WorkflowConsoleApplication7" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mda="clr-namespace:Microsoft.Data.Activities;assembly=Microsoft.Data.Activities" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Sequence sad:XamlDebuggerXmlReader.FileName="c:\users\maurice\documents\visual studio 2010\Projects\WorkflowConsoleApplication7\WorkflowConsoleApplication7\Workflow1.xaml" sap:VirtualizedContainerService.HintSize="316,1018">
    <Sequence.Variables>
      <Variable x:TypeArguments="scg3:List(local:Customer)" Name="Customers" />
    </Sequence.Variables>
    <sap:WorkflowViewStateService.ViewState>
      <scg3:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg3:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <mda:ExecuteSqlQuery x:TypeArguments="local:Customer" ProviderName="{x:Null}" CommandText="Select * from Customers where Country = @Country" ConnectionConfigurationName="WorkflowConsoleApplication7.Properties.Settings.NorthwindConnection" sap:VirtualizedContainerService.HintSize="294,647" Result="[Customers]">
      <mda:ExecuteSqlQuery.RecordProcessor>
        <ActivityFunc x:TypeArguments="sd:IDataRecord, local:Customer">
          <ActivityFunc.Argument>
            <DelegateInArgument x:TypeArguments="sd:IDataRecord" Name="record" />
          </ActivityFunc.Argument>
          <ActivityFunc.Result>
            <DelegateOutArgument x:TypeArguments="local:Customer" Name="target" />
          </ActivityFunc.Result>
          <Sequence DisplayName="Map Customer Fields" sap:VirtualizedContainerService.HintSize="264,476">
            <sap:WorkflowViewStateService.ViewState>
              <scg3:Dictionary x:TypeArguments="x:String, x:Object">
                <x:Boolean x:Key="IsExpanded">True</x:Boolean>
                <x:Boolean x:Key="IsPinned">False</x:Boolean>
              </scg3:Dictionary>
            </sap:WorkflowViewStateService.ViewState>
            <Assign sap:VirtualizedContainerService.HintSize="242,58">
              <Assign.To>
                <OutArgument x:TypeArguments="local:Customer">[target]</OutArgument>
              </Assign.To>
              <Assign.Value>
                <InArgument x:TypeArguments="local:Customer">[New Customer]</InArgument>
              </Assign.Value>
            </Assign>
            <Assign sap:VirtualizedContainerService.HintSize="242,58">
              <Assign.To>
                <OutArgument x:TypeArguments="x:String">[target.CustomerId]</OutArgument>
              </Assign.To>
              <Assign.Value>
                <InArgument x:TypeArguments="x:String">[record.GetString(0)]</InArgument>
              </Assign.Value>
            </Assign>
            <Assign sap:VirtualizedContainerService.HintSize="242,58">
              <Assign.To>
                <OutArgument x:TypeArguments="x:String">[target.CompanyName]</OutArgument>
              </Assign.To>
              <Assign.Value>
                <InArgument x:TypeArguments="x:String">[record.GetString(1)]</InArgument>
              </Assign.Value>
            </Assign>
            <Assign sap:VirtualizedContainerService.HintSize="242,58">
              <Assign.To>
                <OutArgument x:TypeArguments="x:String">[target.ContactName]</OutArgument>
              </Assign.To>
              <Assign.Value>
                <InArgument x:TypeArguments="x:String">[record.GetString(2)]</InArgument>
              </Assign.Value>
            </Assign>
          </Sequence>
        </ActivityFunc>
      </mda:ExecuteSqlQuery.RecordProcessor>
      <InArgument x:TypeArguments="x:String" x:Key="Country">UK</InArgument>
    </mda:ExecuteSqlQuery>
    <ForEach x:TypeArguments="local:Customer" DisplayName="ForEach&lt;Customer&gt;" sap:VirtualizedContainerService.HintSize="294,207" Values="[Customers]">
      <ActivityAction x:TypeArguments="local:Customer">
        <ActivityAction.Argument>
          <DelegateInArgument x:TypeArguments="local:Customer" Name="item" />
        </ActivityAction.Argument>
        <WriteLine sap:VirtualizedContainerService.HintSize="257,100" Text="[item.CustomerId &amp; &quot; &quot; &amp; item.CompanyName &amp; &quot; &quot; &amp; item.ContactName]" />
      </ActivityAction>
    </ForEach>
  </Sequence>
</Activity>

and in design mode:

image

 

Conclusion

These activities work well enough but result in using plain old ADO.NET. Now there is nothing wrong with plain ADO.NET but with the current emphasis on Object Relational Mappers like the Entity Framework this seems a bit out of place. And the option to use connection strings from the application settings instead of embedding them in the workflow isn’t all that obvious either. Still a nice set of activities to add to the toolbox.

 

Enjoy!

www.TheProblemSolver.nl
Wiki.WindowsWorkflowFoundation.eu

Posted by Maurice | 1 comment(s)

Note: This blog post is written using Silverlight 4.0 RC 1

One of the cool new features in Silverlight 4 is the ability to data bind to indexed properties. This means that even if you don’t know at design time what properties you data object has you can still data bind to them.

The syntax is very similar to a normal data binding, only in this case you need to use the [key] syntax instead. For example in example below the FirstName is a regular property while the LastName below is an indexed property.

<StackPanel Name="LayoutRoot">
    <StackPanel Orientation="Horizontal" Margin="1">
        <TextBlock Text="FirstName" Width="150"></TextBlock>
        <TextBox Text="{Binding FirstName, Mode=TwoWay}" Width="500"></TextBox>
    </StackPanel>
    <StackPanel Orientation="Horizontal" Margin="1">
        <TextBlock Text="LastName" Width="150"></TextBlock>
        <TextBox Text="{Binding [LastName], Mode=TwoWay}" Width="500"></TextBox>
    </StackPanel>
</StackPanel>

 

Creating the class to data bind to is simple. All you need to do is add a property with the following syntax:

public object this[string key]
    { get; set; }

 

In this example I am using a Peron class with a regular FirstName property and all others are dong using indexed properties. The complete class, including INotifyPropertyChanged looks like this:

public class Person : INotifyPropertyChanged
{
    public Person()
    {
        PropertyChanged = (s, e) => { };
 
        FirstName = "Maurice";
        this["LastName"] = "de Beijer";
        this["BabyName"] = "Kai";
    }
 
    private Dictionary<string, object> _data = new Dictionary<string, object>();
    public object this[string key]
    {
        get
        {
            if (!_data.ContainsKey(key))
                _data[key] = null;
 
            return _data[key];
        }
        set
        {
            _data[key] = value;
            PropertyChanged(this, new PropertyChangedEventArgs(""));
        }
    }
 
    public IEnumerable<string> Keys
    {
        get
        {
            return _data.Keys;
        }
    }
 
    private string _firstName;
    public string FirstName
    {
        get
        {
            return _firstName;
        }
        set
        {
            _firstName = value;
            PropertyChanged(this, new PropertyChangedEventArgs("FirstName"));
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
}

 

While not having to know the objects structure at design time is nice but in the XAML above I still hard coded the data bindings so that doesn’t buy us much yet. So fully utilize this we need to dynamically generate the UI as well. Fortunately that is quite easy to do with the following code:

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    var person = new Person();
    DataContext = person;
 
    foreach (var item in person.Keys)
    {
        var lbl = new TextBlock();
        lbl.Text = item;
        lbl.Width = 150;
 
        var txt = new TextBox();
        var binding = new Binding("[" + item + "]");
        binding.Mode = BindingMode.TwoWay;
        txt.SetBinding(TextBox.TextProperty, binding);
        txt.Width = 500;
 
        var line = new StackPanel();
        line.Orientation = Orientation.Horizontal;
        line.Children.Add(lbl);
        line.Children.Add(txt);
        line.Margin = new Thickness(1);
        LayoutRoot.Children.Add(line);
    }
}

And the result looks like this. The LastName is printed twice because it is both hard coded into the XAML and added dynamically.

image

 

A few gotchas I ran into.

While doing this I experimented with using a Dictionary<string, object> as the DataContext itself. That worked file except for the part that this doesn’t implement INotifyPropertyChanged. So i tried to create a Peron class deriving from Dictionary<string, object> and adding a indexer to that but this doesn’t work, data binding completely fails to load the data. The same was try for my own implementation of IDictionary<string, object>.

Another thing was the property name that is used with the INotifyPropertyChanged.PropertyChanged event. It seems using an empty string as the property name is the only thing that works to get other controls to update their binding. Not a big issue but I caught me at first.

 

Enjoy!

www.TheProblemSolver.nl
Wiki.WindowsWorkflowFoundation.eu

Posted by Maurice | 4 comment(s)
Filed under: , , ,