The Actipro Blog

All the latest UI control development news from Actipro

SyntaxEditor for WPF - Syntax languages to be service locators

One thing we’ve been working on this week in the world of SyntaxEditor for WPF is the ability for syntax languages to support the display of IntelliPrompt completion lists, and to be able to easily respond to Ctrl+Space, etc. without code having to be written external to the language implementation.  These sort of features are already supported in SyntaxEditor for WinForms.

The WPF version has a bit of a different design however, where the text/parsing framework is in one assembly and the WPF UI portion is in another.  This has been done so that we can eventually reuse the new text/parsing framework on other platforms like WinForms.  The separation is great design-wise however the problem we found was that in order to have some sort of IntelliPrompt provider (defined in the UI assembly) on the ISyntaxLanguage interface (defined in the Text assembly), we’d need to do some non-straightforward things like having a language class optionally implement an interface.

We just weren’t happy with where that was headed so instead decided to change ISyntaxLanguage for the next build to use the service locator designer pattern. 

What is the service locator design pattern?

The service locator’s interface looks like this, which is part of the next build:

   1: public interface IServiceLocator {
   2:     
   3:     IEnumerable<Type> GetAllServices();
   4:     T GetService<T>();
   5:     object GetService(Type serviceType);
   6:     void RegisterService<T>(T service);
   7:     void RegisterService(Type serviceType, object service);
   8:     void UnregisterService<T>();
   9:     void UnregisterService(Type serviceType);
  10:  
  11: }

Essentially you register “services” with each instance of a syntax language.  The services provide some sort of functionality related to the language.  You only need to register services that your language wants to implement.

The major benefit of this is that it’s completely generic and extensible.  So while we have a number of services that we’ll document and will make easy for you to retrieve, you could also associate your own custom services with the language.

What changes are involved in moving from 2009.1 build 500 to this new design?

In build 500, the ISyntaxLanguage had a handful of properties on it.  One property was LineCommenter, which returned an ILineCommenter instance.

With the new design, that property is gone.  Now to get the ILineCommenter, if any, that is associated with the language, you’d do:

   1: var lineCommenter = language.GetService<ILineCommenter>();

To “set” or register a line commenter with a language, you’d do:

   1: language.Register<ILineCommenter>(myLineCommenterInstance);

You can see the strong use of generics that keep the code clean. 

Again, we will be documenting exactly which built-in service interface types can be used with languages.  Even so, the only thing we don’t like about the new extensible architecture is that it’s still not very in-your-face about which built-in service interface types are generally used with languages.

Extension methods to the rescue

We thought about it and decided this would be an excellent opportunity to use extension methods to help provide easier access to the various built-in service interface types.  We’ve created a new SyntaxLanguageExtensions static class in the ActiproSoftware.Text.Parsing namespace that contains a number of extension methods for ISyntaxLanguage.

Here is a partial snippet of the SyntaxLanguageExtensions code that just shows the methods related to line commenters:

   1: public static class SyntaxLanguageExtensions {
   2:     
   3:     public static ILineCommenter GetLineCommenter(this ISyntaxLanguage language) {
   4:         return language.GetService<ILineCommenter>();
   5:     }        
   6:     
   7:     public static void RegisterLineCommenter(this ISyntaxLanguage language,
   8:         ILineCommenter value) {
   9:         language.RegisterService<ILineCommenter>(value);
  10:     }        
  11:     
  12:     public static void UnregisterLineCommenter(this ISyntaxLanguage language) {
  13:         language.UnregisterService<ILineCommenter>();
  14:     }
  15:  
  16: }

With this new class, as long as you are importing the ActiproSoftware.Text.Parsing namespace, you’ll have a slew of new methods on any ISyntaxLanguage.  Instead of using the generic GetService method, you can now do this instead:

   1: var lineCommenter = language.GetLineCommenter();

And to register a line commenter, you can do this:

   1: language.RegisterLineCommenter(myLineCommenterInstance)

Summary

As you can see, by this change we have really opened up the architecture of syntax languages.  It’s now easier than ever to swap custom functionality in for any feature area of a language, and you can register your own custom features as well.

Comments (4) -

June 10, 2009 at 06:14  

Jan Bannister United Kingdom

I really like this approach. The ServiceLocator is really neat dynamic aggregation pattern. And using extensions to show the common services is a nice touch.

June 10, 2009 at 18:44  

Jesper Sweden

You made the right choice, and the extension methods even retains the API coherence. Nice. I like how you sweat the details.

I'm a bit puzzled why there are non-generic alternatives, though. You can cast your way to any scenario they provide. What real-world usage are they supporting? XAML invocation?

June 10, 2009 at 22:56  

Bill Henning (Actipro) United States

We originally were going to only offer the generic methods however the problem is with the GetAllServiceTypes (renamed from GetAllServices).  If you are enumerating those values in code, the generic methods cannot be used.  However the non-generic methods can be used with the Types returned by GetAllServiceTypes.

So the thought is, you could programmatically list all service types registered with a language, then use those types to call the non-generic GetService method to retrieve actual instances of them.

June 11, 2009 at 03:12  

Jesper Sweden

Ah! That makes sense since I suppose you can't pass a type object as a type argument.

Pingbacks and trackbacks (1)+

Comments are closed