Using SpeakToMe – A First Project

December 3, 2012 at 2:51 PMAdministrator

 

As I announced some time ago, I have released my natural language processor to Codeplex.  The documentation on the site is fairly complete, but I wanted to take a different approach on my blog by going at it in more a tutorial fashion starting from downloading the binaries and integrating them into your project.  From there, I will be occasionally posting some tips and tricks that show how flexible the engine is and how to get the most out of it.  Let’s have a look at how to get going with the engine.

Obtaining the Binaries

The first step in building a new project with SpeakToMe is to obtain the binaries containing the types we’ll need.  To get them, go to the Codeplex site’s download page and download the binaries release (SpeakToMe_bin_2012_10_19 as of this writing).  Once the zip file is downloaded, extract the files to a place where you can find them, later.  The zip file should contain three assemblies: Microsoft.Practices.Prism.dll, SpeakToMe.Core.dll and SpeakToMe.Speech.dll.

Create a New Project

Start Visual Studio and create a new WPF project.  The new project dialog should look similar to the below.

Sample Project

Click OK to generate the project.

We need to set reference to the SpeakToMe assemblies.  To do that, create a sub-folder in your new project (I called mine ThirdParty) and copy the three assemblies, mentioned above, into the folder.  Next, in the project references, add references to the three assemblies.

Supporting Users and Conversations

There are a couple of pieces of plumbing we need to build before we get into the fun stuff.  One of these is a data layer that knows how to access information about users and conversations.  Why is this necessary?  SpeakToMe has built in support for multiple users and multiple ways for users to access the engine.  If multiple conversations are going on, the engine needs to be able to figure out how to get a response back to the correct user.  If one user is accessing the system via email and another via chat, there must be a way to insure the correct channel is used to send the response.  So, for the purposes tracking this information, some form of data storage must be implemented.

SpeakToMe does not dictate how you do this.  Instead, it provides an interface describing the needed functionality which you must implement.  There are also types the engine expects to work with which are defined for you.  Let’s see how to pull this all together by building the necessary parts.

Creating a Data Store

The example code that is included in the SpeakToMe source download includes an embedded database implementation.  We’ll use the same schema to create a new database on a local instance of SQL Express.

I have created a new database named SpeakToMe and have created three tables in that database named “Users”, “Conversations” and “ConversationHistory”.  The schema looks like this.

SpeakToMe_Schema

Let’s take a moment to discuss the data that will be stored here and what each column holds.  First of all, each table has an integer key column that is auto-incrementing.  Now, starting with the Users table, the column are pretty self-explanatory where the user name is for display in tooling and the first and last name can be used anywhere.  Next, look at the conversation table.  Each entry in this table describes a single conversation with a single user.  The initiated and active columns describe when the conversations started and whether or not it is still active.  In some cases we are able to determine whether the user has ended the conversation and in other situations we cannot.  If a user ends an IM session with us, we can assume the conversation is over.  If we get an email from a user, we can’t determine whether or not we’ll receive another, so conversations for this type of communication tend to just stay open.

NOTE: I won’t be discussing building functionality for communicating over email and IM in this article, but stay tuned!

The Mode and Address columns are used to determine what channel the communication came from.  Mode is defined as an enumeration in the Core assembly, so we know how we got the message.  However, what if a single user initiates two distinct conversations from two different email accounts?  We include address, here, to make the distinction between the two.  Lastly, UserId denotes the user we’re talking to.

The ConversationHistory table contains each individual communication between the user and the system.  Message and MessageDateTime contain the text of the message and when it was sent.  UserInitiated is true if the message came from the user and is false if it originated with the system.  Tag and TagType are used to store a piece of state along with a particular message so it can be retrieved and referenced later on in the conversation.

Be sure to add the foreign keys between the tables so you’ll be able to access table references on your entities.  I find that creating a Database Diagram is an easy way to do this.

dbdiag

 

Creating a Data Access project.

For this example, I’m going to be using the Entity Framework to access data so I need to create a class library project that will contain my data access code and create a conceptual model.

Add a new class library project to the solution you started.  I’m calling mine “SpeakToMe.Sample.Data”.  Next, delete the class1.cs file that is generated for you.

Now, we need to add the Entity Framework to our data project.  We’ll use NuGet to do this by right-clicking on the references note in the project and selecting “Manage NuGet packages…”.  When the dialog appears, enter “entity framework” into the search box.  The list should populate.  Select the entry in the list entitled “EntityFramework” and click the install button.

addef

After the libraries have been installed into your project, close the NuGet dialog.  Now we need to create our model from the database.  Right-click your project and select Add->New Item.  Select “ADO.Net Entity Data Model” and specify a name for your model.  I named mine “SpeakToMe”.  Click the “Add” button.

AddModel

In the wizard, select “Generate from database” and click next.  Now, click the Create New Connection button and select your server (local for me) and the database you created.

CreateConnection

Click the “Test Connection” button to ensure the information is correct.  If so, click OK.  As a convenience, you might want to leave the Save Connection String checkbox checked.  This will create the connectionstring entry in your config file for use later.

save_connection

Click Next and wait for the database information to populate in the next screen.  Now, drill down to the tables you created and select all of them.

TableList

Click “Finish” and your model will be created.

Implementing the IUserData Interface

We need to add a class, now, that implements the IUserData interface.  In your data project, add a class named UserData and add the attributes shown below.

UserData Class
  1. namespace SpeakToMe.Sample.Data
  2. {
  3.     [Export(typeof(IUserData))]
  4.     [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)]
  5.     public class UserData : IUserData
  6.     {
  7.         public Core.Models.ConversationHistory AddConversationHistory(int conversationId, string text, string tagString, string tagType, bool userInitiated)
  8.         {
  9.             throw new NotImplementedException();
  10.         }
  11.  
  12.         public Core.Models.Conversation CreateConversation(int userId, Core.Enums.ConversationType type, string address)
  13.         {
  14.             throw new NotImplementedException();
  15.         }
  16.  
  17.         public Core.Models.User CreateUser(string userName, string firstName, string lastName)
  18.         {
  19.             throw new NotImplementedException();
  20.         }
  21.  
  22.         public Core.Models.Conversation GetConversation(int userId, Core.Enums.ConversationType type, string address)
  23.         {
  24.             throw new NotImplementedException();
  25.         }
  26.  
  27.         public List<Core.Models.ConversationHistory> GetConversationHistory(int conversationId)
  28.         {
  29.             throw new NotImplementedException();
  30.         }
  31.  
  32.         public Core.Models.User GetUserById(int id)
  33.         {
  34.             throw new NotImplementedException();
  35.         }
  36.     }
  37. }

Note that you’ll need to set references to SpeakToMe.Core and System.ComponentModel.Composition in order to be able to successfully compile this class.  As for the attributes on the class, these allow the use of Microsoft Extensibility Framework (MEF).  SpeakToMe uses MEF as a IoC container and the engine will use it to obtain an instance of this class when it needs to.

Now, we need to implement the methods on the interface.  Below is my implementation.

Completed UserData Class
  1. using SpeakToMe.Core.Interfaces;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel.Composition;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8.  
  9. namespace SpeakToMe.Sample.Data
  10. {
  11.     [Export(typeof(IUserData))]
  12.     [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)]
  13.     public class UserData : IUserData
  14.     {
  15.         public Core.Models.ConversationHistory AddConversationHistory(int conversationId, string text, string tagString, string tagType, bool userInitiated)
  16.         {
  17.             using (var ctx = new SpeakToMeEntities())
  18.             {
  19.                 //create new database entry using entity defined by EF
  20.                 SpeakToMe.Sample.Data.ConversationHistory ch = new ConversationHistory
  21.                 {
  22.                     ConversationId = conversationId,
  23.                     Message = text,
  24.                     MessageDateTime = DateTime.Now,
  25.                     Tag = tagString,
  26.                     TagType = tagType,
  27.                     UserInitiated = userInitiated
  28.                 };
  29.  
  30.                 ctx.ConversationHistories.Add(ch);
  31.                 ctx.SaveChanges();
  32.  
  33.                 //return an instance of the entity.  Note that this is not the entity defined by EF, but the one defined in SpeakToMe.Core which is the only one the engine would know about.
  34.  
  35.                 SpeakToMe.Core.Models.ConversationHistory conversationHistory = new Core.Models.ConversationHistory
  36.                 {
  37.                     ID = ch.ConversationId,
  38.                     Message = ch.Message,
  39.                     MessageDateTime = ch.MessageDateTime,
  40.                     Tag = ch.Tag,
  41.                     TagType = ch.TagType,
  42.                     UserInitiated = ch.UserInitiated
  43.                 };
  44.  
  45.                 return conversationHistory;
  46.  
  47.             }
  48.  
  49.         }
  50.  
  51.         public Core.Models.Conversation CreateConversation(int userId, Core.Enums.ConversationType type, string address)
  52.         {
  53.             using (var ctx = new SpeakToMeEntities())
  54.             {
  55.                 Conversation conv = new Conversation
  56.                 {
  57.                     Active = true, //must be true if we're creating a conversation
  58.                     Address = address,
  59.                     Initiated = DateTime.Now,
  60.                     Mode = (int)type,
  61.                     UserId = userId
  62.                 };
  63.  
  64.                 ctx.Conversations.Add(conv);
  65.                 ctx.SaveChanges();
  66.  
  67.                 Core.Models.Conversation conversation = new Core.Models.Conversation
  68.                 {
  69.                     Active = conv.Active,
  70.                     Address = conv.Address,
  71.                     ID = conv.ConversationId,
  72.                     Initiated = conv.Initiated,
  73.                     Mode = (Core.Enums.ConversationType) conv.Mode,
  74.                     UserId = conv.UserId
  75.                 };
  76.  
  77.                 return conversation;
  78.             }
  79.         }
  80.  
  81.         public Core.Models.User CreateUser(string userName, string firstName, string lastName)
  82.         {
  83.             using (var ctx = new SpeakToMeEntities())
  84.             {
  85.                 User usr = new User
  86.                 {
  87.                     FirstName = firstName,
  88.                     LastName = lastName,
  89.                     UserName = userName
  90.                 };
  91.  
  92.                 ctx.Users.Add(usr);
  93.                 ctx.SaveChanges();
  94.  
  95.                 Core.Models.User user = new Core.Models.User
  96.                 {
  97.                     FirstName = usr.FirstName,
  98.                     LastName = usr.LastName,
  99.                     ID = usr.UserId,
  100.                     UserName = usr.UserName
  101.                 };
  102.  
  103.                 return user;
  104.             }
  105.         }
  106.  
  107.         public Core.Models.Conversation GetConversation(int userId, Core.Enums.ConversationType type, string address)
  108.         {
  109.             using (var ctx = new SpeakToMeEntities())
  110.             {
  111.                 Core.Models.Conversation conversation = ctx.Conversations.Where(c => c.UserId == userId && c.Mode == (int)type && c.Address == address).Select(c => new Core.Models.Conversation
  112.                     {
  113.                         Active = c.Active,
  114.                         Address = c.Address,
  115.                         ID = c.ConversationId,
  116.                         Initiated = c.Initiated,
  117.                         Mode = (Core.Enums.ConversationType)c.Mode,
  118.                         UserId = c.UserId
  119.                     }).FirstOrDefault();
  120.  
  121.                 return conversation;
  122.  
  123.             }
  124.         }
  125.  
  126.         public List<Core.Models.ConversationHistory> GetConversationHistory(int conversationId)
  127.         {
  128.             List<Core.Models.ConversationHistory> messages = new List<Core.Models.ConversationHistory>();
  129.             using (var ctx = new SpeakToMeEntities())
  130.             {
  131.                 var msgs = ctx.ConversationHistories.Where(c => c.ConversationId == conversationId);
  132.  
  133.                 msgs.ToList().ForEach(m =>
  134.                     {
  135.                         messages.Add(new Core.Models.ConversationHistory
  136.                             {
  137.                                 ID = m.ConversationHistoryId,
  138.                                 Message = m.Message,
  139.                                 MessageDateTime = m.MessageDateTime,
  140.                                 Tag = m.Tag,
  141.                                 TagType = m.TagType,
  142.                                 UserInitiated = m.UserInitiated
  143.                             });
  144.                     });
  145.  
  146.                 return messages;
  147.             }
  148.         }
  149.  
  150.         public Core.Models.User GetUserById(int id)
  151.         {
  152.             using (var ctx = new SpeakToMeEntities())
  153.             {
  154.                 var usr = ctx.Users.Where(u => u.UserId == id).FirstOrDefault();
  155.  
  156.                 if (usr == null)
  157.                     return null;
  158.  
  159.                 return new Core.Models.User
  160.                 {
  161.                     FirstName = usr.FirstName,
  162.                     LastName = usr.LastName,
  163.                     ID = usr.UserId,
  164.                     UserName = usr.UserName
  165.                 };
  166.             }
  167.         }
  168.     }
  169. }

 

Presence

With SpeakToMe, a particular presence is a mode of communication.  In this post, I have already mentioned email and IM.  You could also implement SMS or any other communication protocol you desire.  In this sample, we’re going to create a simple test presence that will serve as the channel between our WPF application and SpeakToMe.  In a later post, I’ll describe how to build an XMPP (IM) presence.

Let’s create another new class library project to hold our presence class.  I’m calling mine SpeakToMe.Sample.Presence.  Delete the Class1.cs file that is created by default and create a new class called TestPresence.  Now add a references to SpeakToMe.Core and System.ComponentModel.Composition and make your class declaration look like this.

Code Snippet
  1. [Export(typeof(IPresence))]
  2. [PartCreationPolicy(CreationPolicy.Shared)]
  3. public class TestPresence : IPresence
  4. {
  5.     public void Initialize()
  6.     {
  7.         throw new NotImplementedException();
  8.     }
  9.  
  10.     public bool IsConnected
  11.     {
  12.         get { throw new NotImplementedException(); }
  13.     }
  14.  
  15.     public void ProcessCommand(string command, int userId, ISmartHomeServiceCallback callback)
  16.     {
  17.         throw new NotImplementedException();
  18.     }
  19.  
  20.     public void OnImportsSatisfied()
  21.     {
  22.         throw new NotImplementedException();
  23.     }

A couple of things to note, here.  First of all, you can tell from the class attributes that SpeakToMe is going to use MEF to load an instance of this class when it’s needed.  Also, this class is implementing another interface that is provided in SpeakToMe.Core.  Any class that will serve as another presence for the system will need to be exported this way and will also need to implement this same interface.  This approach allows you to add as many communication mechanisms as you like without having to touch the source code of the library. 

When implementing the interface, the Initialize method should be used to get everything set up for listening to a channel and sending messages through it.  For example, connecting to an email server or an XMPP server.  The IsConnected property should report whether or not the channel is open for communication.  If a connection is lost, for example, the property should return false.  All processing by the way of sending a message into the SpeakToMe engine should be handled in the ProcessCommand method.  This method has a void return type because we will be notified via an event when the response is ready to be sent back to the user.  Lastly, the OnImportsSatisfied will be called when MEF has supplied all the instances we have requested of it.  More on this in a second.  Now, set references to the SpeakToMe.Core, SpeakToMe.Speech and Microsoft.Practices.Prism assemblies we copied into the third party folder earlier.  Here is my implementation of the TestPresence class.

Completed TestPresence Class
  1. [Export(typeof(IPresence))]
  2.     [PartCreationPolicy(CreationPolicy.Shared)]
  3.     public class TestPresence : IPresence
  4.     {
  5.         [Import]
  6.         public IEventAggregator EventAggregator { get; set; }
  7.  
  8.         public bool IsConnected
  9.         {
  10.             get { return true; }
  11.         }
  12.  
  13.         public void Initialize()
  14.         {
  15.             //do nothing
  16.         }
  17.  
  18.         private void ReplyToChannelEventHandler(ReplyToChannelEventArgs args)
  19.         {
  20.             if (args.Mode == ConversationType.Test)
  21.             {
  22.                 //ConversationData.CreateConversationHistory(args.ConversationId, args.Reply, args.TagString, args.Tag, false);
  23.  
  24.                 if (args.Callback != null)
  25.                 {
  26.                     args.Callback.ReturnResult(args.Reply);
  27.                 }
  28.             }
  29.         }
  30.  
  31.         public void OnImportsSatisfied()
  32.         {
  33.             this.EventAggregator.GetEvent<ReplyToChannelEvent>().Subscribe(ReplyToChannelEventHandler);
  34.         }
  35.  
  36.         public void ProcessCommand(string command, int userId, ISmartHomeServiceCallback callback)
  37.         {
  38.             var processor = ServiceLocator.GetInstance<CommandProcessor>();
  39.             processor.ProcessCommand(command, userId, ConversationType.Test, "", callback);
  40.         }

Off the bat, you’ll notice the EventAggregator property and its associated [Import] attribute.  The attribute is one way we can ask MEF for an instance of a type.  If you look at the OnImportsSatisfied method, you’ll see we’re setting up an event listener there.  We want to do this setup here, because we are using an instance of the EventAggregator type and we want to make sure MEF has given it to us before we start using its reference.  For our sample, we’ll be calling ProcessCommand directly from our client and this method will pass the message on to the NLP engine for processing.  When processing is complete, we’ll be called back on the ReplyToChannelEventHandler where we call the passed in callback to send the reply back to the caller.  For other communication types, we might be sending an email or IM message instead of calling the callback.

The Bootstrapper

I mentioned, earlier, that SpeakToMe uses MEF to load types.  MEF provides an easy way to extend a system without having to put explicit code dependencies in place.  If you’re not familiar with MEF and how it works, I’d suggest you take a bit of time to look at the documentation here.

In a nutshell, however, MEF utilizes a catalog of types.  In this case, we need to point MEF at assemblies we want it to look at.  It will go through the types in the assembly and identify any that have an Export attribute on them.  These are then added to the catalog.  Now, when code needs an instance of the type, it can ask MEF to get one from the catalog.

There are many ways to specify to MEF which assemblies to include.  In our case, we’re going to do this in code.  SpeakToMe includes a BootStrapperBase class that automatically loads the assemblies it knows about.  But, hey, we just added those data access and presence projects and we need a way to tell MEF about them.  This is what the Bootstrapper class is for.  I should also note, however, that it is absolutely required that you implement the bootstrapper whether you have additional assemblies to load or not because without doing so, the default assemblies will never be loaded. 

In the WPF application you created at the beginning of this exercise, create a new class and call it Bootstrapper.  You’ll need to set a reference from that project to SpeakToMe.Speech.  Now, have the new class inherit from BootStrapperBase and override the AddCustomAssembliesToCatalog method

Bootstrapper
  1. public class Bootstrapper : BootStrapperBase
  2.     {
  3.         protected override void AddCustomAssembliesToCatalog(System.ComponentModel.Composition.Hosting.AggregateCatalog catalog)
  4.         {
  5.             base.AddCustomAssembliesToCatalog(catalog);
  6.         }
  7.     }

 

It is in this overridden method that we will add our custom assemblies like so.

Adding Assemblies To Catalog
  1. catalog.Catalogs.Add(new AssemblyCatalog(typeof(UserData).Assembly));
  2. catalog.Catalogs.Add(new AssemblyCatalog(typeof(TestPresence).Assembly));

You’ll need to add references to your projects in order to get a clean compile.

The client

Now that we’ve added all the required plumbing, we can start working on the client we’re going to use to communicate with the engine.  I’m going to start with the default MainWindow.xaml file that was created when we created our WPF project and add a ListBox, TextBox and Button to it like this.

window

The listbox is at the top a will contain our conversation.  The textbox is at the bottom and will be used to specify our input.  The button sends the input to the engine.  Now let’s name the controls and clean up a bit.  Here is the resulting Xaml.

Main Window Xaml
  1. <Window x:Class="SpeakToMe.Sample.MainWindow"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         Title="MainWindow" Height="350" Width="525">
  5.     <Grid>
  6.         <ListBox HorizontalAlignment="Left" Height="248" Margin="10,10,0,0" VerticalAlignment="Top" Width="497" x:Name="Conversation"/>
  7.         <TextBox HorizontalAlignment="Left" Height="34" Margin="25,275,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="397" x:Name="Message"/>
  8.         <Button Content="Say" HorizontalAlignment="Left" Margin="432,275,0,0" VerticalAlignment="Top" Width="75" Height="34" x:Name="SayButton"/>
  9.  
  10.     </Grid>
  11. </Window>

Let’s do some work in the code behind of this window, now.

First, let’s add some code to the constructor to create and initialize our MEF catalog using the bootstrapper class we created.

Code Snippet
  1. public MainWindow()
  2.       {
  3.           Bootstrapper bs = new Bootstrapper();
  4.           bs.Initialize();
  5.           InitializeComponent();
  6.       }

Now, we need to add an event handler to the button so we can do something when it’s clicked.

Here is the class after adding the event handler.

Code Snippet
  1. using SpeakToMe.Core;
  2. using SpeakToMe.Core.Interfaces;
  3. using SpeakToMe.Sample.Presence;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows;
  10. using System.Windows.Controls;
  11. using System.Windows.Data;
  12. using System.Windows.Documents;
  13. using System.Windows.Input;
  14. using System.Windows.Media;
  15. using System.Windows.Media.Imaging;
  16. using System.Windows.Navigation;
  17. using System.Windows.Shapes;
  18.  
  19. namespace SpeakToMe.Sample
  20. {
  21.     /// <summary>
  22.     /// Interaction logic for MainWindow.xaml
  23.     /// </summary>
  24.     public partial class MainWindow : Window
  25.     {
  26.         public MainWindow()
  27.         {
  28.             Bootstrapper bs = new Bootstrapper();
  29.             bs.Initialize();
  30.             InitializeComponent();
  31.         }
  32.  
  33.         private void SayButton_Click_1(object sender, RoutedEventArgs e)
  34.         {
  35.             this.Dispatcher.Invoke(new Action(() => this.Conversation.Items.Add(Message.Text)));
  36.             IPresence presence = ServiceLocator.GetInstance<TestPresence>();
  37.             presence.ProcessCommand(Message.Text, 1, new CallbackWrapper(new Action<string>((msg) =>
  38.             {
  39.                 this.Dispatcher.Invoke(new Action(() =>
  40.                 {
  41.                     Conversation.Items.Add(msg);
  42.                 }));
  43.             })));
  44.         }
  45.     }
  46. }

 

The first thing we do after the button is clicked is to add our message to the system to the listbox.  Next, we use the ServiceLocator to get an instance of our presence class.  The ServiceLocator is a class we can use to explicitly get an instance of a class from MEF.  If you’ll recall, we exported this class and it should be in our catalog.  Once we have an instance, we call ProcessCommand on that instance and pass in the message, a userId and a callback to be called when a response is ready.  All the callback does is add the system’s response to the listbox so we can observe it.

Initializing the Data in the Database

The last task we have to do before testing is to add some data to the database.  Use whatever tools you need to to add a single user to the users table.  You can specify whatever values you like for the columns.  Just make sure that the id you pass in the ProcessCommand call matches your user’s ID.

We also need an active conversation to be manually created for this test.  Create a new record in the Conversations table for this with the initiated value set to any datetime value, active set to true, mode = 0, address null and userId set to the id of the user you created.

One last thing.  When we were creating our entity model, a connection string was placed in the app.config file for the data project.  You’ll need to copy this connection string and paste it into the app.config file of your WPF app.

Testing

That completes the coding for the test.  Be sure to set the WPF application as the startup project and press F5.  The main window should open.  Type “hello” into the textbox and press the button.  After a second or two, you should see “Hello” followed by your user’s first name appear in the listbox.  This is the response from the system.  Congratulations!  You have just created an application that makes use of the SpeakToMe NLP engine.

Conclusion

Actually, this is more of a beginning than a conclusion.  I will be posting several posts pertaining to the SpeakToMe engine in the coming months.  I’ll cover authoring new token classes, building rules and creating an XMPP presence for the engine.  Along the way, I’ll be pointing out specific best practices and tips.  Hope to have you join me.

Once again, I’d like to let everyone know that I welcome and would greatly appreciate any community participation in this effort.  Contact me through comments on this blog or the SpeakToMe Codeplex site for details on getting started.

Posted in: Natural Language Processing | MEF | .Net | Database | SpeakToMe

Tags: , , , ,

A Completely Dynamic View Model

December 29, 2011 at 11:43 AMAdministrator

Recently, I was working on a project where there was a requirement to print certain documents.  These documents were stored, in pieces, in the database and then loaded and assembled at runtime.  This allowed parts of the documents to be reused as needed.

The Xaml for these documents contained bindings to data that originated from the database.  The usual drill would be to statically define all the required properties on the view model and then write code to populate them from the database.  There's nothing wrong with this approach, but the project was in a mode where new documents were being generated quickly and they required new properties to be defined on the view model making the view model code a hot spot.

The data required in the bindings was largely stored as name/value pairs in the database.  Wouldn't it be much nice if new values added to the database just showed up at runtime without having to create new properties and the code to populate them?

Using dynamic objects would be a great way to go, if you could bind to them.  Unfortunately, they don't support reflection which is required for binding.  A second problem is that adding properties to a dynamic object requires a syntax like this:

Connect to XMPP
  1. dynamic vm = new object();
  2.             vm.PropertyName = 5;

Which, of course, requires you to know all property names to be added, ahead of time.  Since I'd like to be able to add properties that I find in the database, this is unsuitable.

I did some research and found this blog post by Lester Lobo.  The solution accompanying the post contained this class.

Connect to XMPP
  1. using System.Collections.Generic;
  2. using System.Collections.ObjectModel;
  3. using System.ComponentModel;
  4. using System.Dynamic;
  5. using System.Windows.Data;
  6. using System;
  7.  
  8. namespace DynamicVM
  9. {
  10.     public class DynamicObjectClass : DynamicObject, INotifyPropertyChanged
  11.     {
  12.         #region DynamicObject overrides
  13.  
  14.         public DynamicObjectClass()
  15.         {
  16.         }
  17.  
  18.         public override bool TryGetMember(GetMemberBinder binder, out object result)
  19.         {
  20.             return members.TryGetValue(binder.Name, out result);
  21.         }
  22.  
  23.         public override bool TrySetMember(SetMemberBinder binder, object value)
  24.         {
  25.             members[binder.Name] = value;
  26.             OnPropertyChanged(binder.Name);
  27.             return true;
  28.         }
  29.  
  30.         public override IEnumerable<string> GetDynamicMemberNames()
  31.         {
  32.             return members.Keys;
  33.         }
  34.  
  35.         public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
  36.         {
  37.             int index = (int)indexes[0];
  38.             try
  39.             {
  40.                 result = itemsCollection[index];
  41.             }
  42.             catch (ArgumentOutOfRangeException)
  43.             {
  44.                 result = null;
  45.                 return false;
  46.             }
  47.             return true;
  48.         }
  49.  
  50.         public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
  51.         {
  52.             int index = (int)indexes[0];
  53.             itemsCollection[index] = value;
  54.             OnPropertyChanged(System.Windows.Data.Binding.IndexerName);
  55.             return true;
  56.         }
  57.  
  58.         public override bool TryDeleteMember(DeleteMemberBinder binder)
  59.         {
  60.             if (members.ContainsKey(binder.Name))
  61.             {
  62.                 members.Remove(binder.Name);
  63.                 return true;
  64.             }
  65.             return false;
  66.         }
  67.  
  68.         public override bool TryDeleteIndex(DeleteIndexBinder binder, object[] indexes)
  69.         {
  70.             int index = (int)indexes[0];
  71.             itemsCollection.RemoveAt(index);
  72.             return true;
  73.         }
  74.  
  75.         #endregion DynamicObject overrides
  76.  
  77.         public void AddProperty(string propertyName, object value)
  78.         {
  79.             members[propertyName] = value;
  80.         }
  81.  
  82.         #region INotifyPropertyChanged
  83.  
  84.         public event PropertyChangedEventHandler PropertyChanged;
  85.  
  86.         private void OnPropertyChanged(string propertyName)
  87.         {
  88.             if (PropertyChanged != null)
  89.                 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  90.         }
  91.  
  92.         #endregion INotifyPropertyChanged
  93.  
  94.         #region Public methods
  95.  
  96.         public object AddItem(object item)
  97.         {
  98.             itemsCollection.Add(item);
  99.             OnPropertyChanged(Binding.IndexerName);
  100.             return null;
  101.         }
  102.  
  103.         #endregion Public methods
  104.  
  105.         #region Private data
  106.  
  107.         Dictionary<string, object> members = new Dictionary<string, object>();
  108.         ObservableCollection<object> itemsCollection = new ObservableCollection<object>();
  109.  
  110.         #endregion Private data
  111.     }
  112.  
  113. }

This class was almost just what I needed.  It solved the problem of not being able to bind to a dynamic, but I still needed a way to add properties on the fly.  For this, I added the AddProperty() method, which you'll see in the above listing.

Using the class is easy.  Below is an example.

Connect to XMPP
  1. AValue = new DynamicObjectClass();
  2.             AValue.Foo = "Hello"; //use the out-of-the-box syntax for adding a property
  3.  
  4.             AValue.AddProperty("Bar", 5); //add a property discovered at runtime.
  5.             AValue.AddProperty(propName, propValue);

So, using the above, I can simply read the name/value pairs out of the database and then add each to my view model at  runtime.  This means that any new value added to the database will automatically be available on the now dynamic view model for binding.

One Offs

Of course, you don't have to use a completely dynamic view model.  It's just as easy to define properties at design time and have one of those properties be a dynamic type.  Also, it's possible to have a dynamic view model that contains a property that is, itself, a dynamic.

Posted in: .Net | Database | WPF | dynamic

Tags: , , ,

Common Table Expressions and Recursion

November 12, 2007 at 4:48 AMmgordon

Some time ago, I was working on a project that needed to work with users as a hierarchy.  The hierarchy was similar to what you would see in typical organizational chart and the users were organized by who they worked for and who worked for them.  As part of one of the requirements for the system, I needed to be able to retrieve from the database any particular user and all users who were placed beneath them in the chart, from their position in it all the way to the bottom.

To store the relationship between all the users, I created a self referencing table that contained user_id and managing_user_id columns.  The idea was that the record would contain the id of a user and also the id of the user over them.  If the user was at the top of the hierarchy, the managing_user_id column would be null.

Obviously, getting the data out of the table in the way I needed it could best be done with recursion.  I needed to get the user, all records that pointed to that user as the managing user...all the records that pointed to each of those records and so on.  I was wondering how to do this without making multiple calls to the database, however, because I could see the number of calls being astronomical for users near the top of the hierarchy.

Then I ran across a technique whereby I could use a CTE in a  recursive way.  The code looked something like this.

      WITH userHierarchyTiers (user_id, managing_user_id, user_hierarchy_id) AS
      (
           SELECT uh.user_id, managing_user_id, user_hierarchy_id
           FROM user_hierarchy uh, users u
           WHERE uh.user_id = @UserId
           AND u.user_id = uh.user_id

           UNION ALL

           SELECT uh.user_id, uh.managing_user_id, uh.user_hierarchy_id
           FROM user_hierarchy uh 
           INNER JOIN userHierarchyTiers uht ON uh.managing_user_id = uht.user_id
      )
      SELECT user_id, managing_user_id from userHierarchyTiers

The key to this technique is in the fact that the second query in the CTE references the entire CTE in its FROM clause.  The net effect is that the CTE is executed and for each row in the result, the CTE is executed again and so on until there are no rows in the result set.

Posted in: Sql Server 2005 | Database

Tags: ,

Bitten by Escaping Single Quotes

August 17, 2007 at 11:55 AMmgordon

I've known about it and handled it well for years.  You know...when you have a string with a single quote embedded in it and you need to insert the string into a database column.  For the longest time, I simply replaced any quotes with two quotes and all was well.  ADO.Net was smarter than I gave it credit for, however, and it apparently hung me up.

 I've been working on a brand new client-server application for the past several months.  I created the dal that was to be used by the entire application and it had been working well.  We just started allowing some user acceptance testing and it happened.  A user keyed in a string with an apostrophe in it and the application thre an exception.  "No problem", I thought, "I'll just replace any quotes with two quotes and I'm on my way".

One of the developers on this project approached me,  shortly after,  and pointed out that quotes I was doubling in the dal were, in fact, being saved in the database as double quotes.  Huh?  After two hours of debugging and tweaking I figured out the problem.  I found a post on Scott Gutherie's blog that pointed out that the SqlParameter class automatically takes care of escaping single quotes.   So why was it seemingly ignoring my quotes?

Turns out that the problem laid with the creation of the actual parameters.  I had created an interface and my Dal class implemented it.  Besides the other benefits to ding this, I wanted to be able to write specific implementations for different databases and be able to switch them out, if needed, without changing any of the application code.  So, necessarily, all the application code was written against the interface and not the specific dal class.  A side effect of this was that the code that called into the dal could not create specific parameter clases such as SqlParameter, but rather it asked the dal to create a parameter specific to the database the class was written for, and the parameter was stored in a variable of type  IDataParameter.

Long story short, the dal contained a method called CreateParameter that looked like this

Public Function CreateParameter(ByVal name As String, ByVal value As Object) As IDataParameter
   
Implements IDAL.CreateParameter
   
   
Return New SqlParameter(name, value)
End Function 'CreateParameter

Note that this method does not specify a DBType for the parameter.  Apparently, the lack of a type confused the SqlParameter class.  I modified the dal to check each parameter being passed in to see if it was a string type.  If it was, I manually set it to a DBType of Varchar.  This worked, in my case, because our standard was to use varchars for all string data types unless a char made more sense.  At any rate, this modification allowed me to remove the code that manually escaped the single quote.  The SqlParameter class now had all it needed to handle the situation on its own.

Posted in: .Net | Sql Server 2005 | Database

Tags: , ,

The DAL is a click away

June 11, 2007 at 11:00 AMmgordon
I recently started working on a personal project and decided to give Subsonic a try at the same time.  Subsonic is a DAL generation tool and it simply rocks!  I spent maybe an hour or so watching the helpful videos provided on the site to learn the basics of setting the tool up, configuring it in the .config file and wiring it up to visual studio as an external tool.  After it was set up, I was generating data transport/business classes and data access code from my database in no time.   Subsonic generates business objects and collections that know how to retrieve by criteria and how to save themselves.  Stored procedures are supported, as well.  While I'm not yet a big fan of the query tool, it can be used to generate queries in code without resorting to managing dynamic sql as strings.  I tend to be old school in some ways and still prefer to use stored procedures to manipulate database.  Even so, Subsonic offers me the ability to call any stored procedure on my database with a single line of code.  It supports Sql Server, MySql and Oracle. Again, I love being more productive.  Subsonic helps me be as productive as possible by relieving me of having to write as much code.

Posted in: .Net | Productivity | Database

Tags: , ,