Channels ▼
RSS

Web Development

Sweetening the Plain Vanilla .NET FileSystemWatcher


Creating the Watcher Builder

When I build this kind of library, I like to separate the configuration of the component from the actual creation of the component internally. I use a common module defining the configuration syntax, which I share between a builder and the actual implementation. When the user starts using the DSL, he's actually talking to the builder and the builder object creates the implementation instance a little bit later. That way, chance of errors is lower and the syntax is explicitly defined separately so it's easier for somebody else to learn. We'll start with the parts of the builder component that can exist on their own without any other classes. Listing 2 shows the output of the specs for that part.


loading the library
- should raise an exception when IRONRUBY_VERSION is undefined

WatcherSyntax
when initializing
- should raise an error when no path is given
- should have a path
- should have an empty filters collection when no filter is provided
- should register the filters
- should by default not recurse in subdirs
- should register and execute the block

when initialized
- should allow setting the path
- should allow setting extra filters
- should disable recursing into subdirs
- should enable recursing into subdirs

when registering handlers
- should register a handler without a filter
- should register a handler with a filter
- should register a handler by method name

Listing 2: Spec output for the first pass over the Builder

The specs above are actually about the behavior that will be shared between the builder and the watcher implementation object. Implementing this class would bring us pretty close to where we need to be in terms of syntax. Afterwards, it's a matter of providing an entry point and actually building the implementation.


Playing Nice With Other Libraries

The library we are creating will only work on IronRuby. It might be good form to inform other Ruby implementations that this library needs to be loaded from IronRuby to work.

There are several ways to solve that and most of them involve checking for a constant or the value of a constant. In this case, I opted to check for the constant IRONRUBY_VERSION, because I know that constant is unique for the IronRuby implementation.

Shri Borde of the IronRuby/DLR developer team recommends the following statement to check if you're running in IronRuby:


do_some_ironruby_stuff if defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "ironruby".

The latter way is probably the proper way of checking for the Ruby platform you're running on.


Because I know where this is heading, I'm going to include this straight into a module. I started out with this as a part of the WatcherBuilder class and later extracted it into the WatcherSyntax module shown in Listing 3.


module WatcherSyntax
  
  attr_accessor :path, :filters, :subdirs, :handlers
  
  def path(val = nil)                                                     #1
    @path = val if val
    @path
  end

  def filter(*val)                                                        #2
    @filters = register_filters @filters, *val
  end

  def top_level_only                                                      #3
    @subdirs = false
  end

  def recurse
    @subdirs = true
  end
  alias_method :include_subdirs, :recurse

  def on(action, *filters, &handler)                                      #4
    @handlers ||= {}
    @handlers[action.to_sym] ||= {}   
    hand = @handlers[action.to_sym] 
    filters = [:default] if filters.empty?
    filters.each do |filt|
      filt = :default if filt.to_s.empty?
      hand[filt] ||= []
      hand[filt] << handler
    end
  end  
  
private 
  def register_filters(coll, *val)
    val.inject(coll||[]) { |memo, filt| memo << filt unless memo.include?(filt); memo  }
  end
  
end

#1 Overloading the path attribute getter
#2 Registering filters
#3 Switching for subdirectories
#4 Registering event handlers

Listing 3: The common syntax module.

The code in Listing 3 first overloads the path getter (#1) that is created by the attr_accessor method. When the provided value is not nil, it will set the path value. Next, we define the filter method (#2). This can be used to register extra filters. The actual registration of a filter is handled by a private method register_filters that ensures the new filters are added only if they don't already exist. The next bit is two methods where one is the inverse of the other to function as a switch to flick on recursing into subdirectories or not (#3). The last method is the on method (#4), which is probably the most complex method in this chunk of code.

The reason that this code is more complex than it could be is because I didn't create a proper class to encapsulate a handler registration but instead I am using a Hash as data structure. As a consequence, it also needs to be initialized with default values. After the data structure initialization, it will add the handler to the registration's handler collection and it will register the filters in the filter collection for this handler. When the filters parameter of the on method is empty, it gets initialized with a :default symbol for the key in the Hash.

If we were to include the module presented here at this point into a class we'd pass about 90% of the specs from Listing 2. We'll now write some specs for the methods that are specific to a builder:


WatcherBuilder
when building watches
- should create a watcher
- should register a new watcher in the watcher bucket
- should register a new watcher and start it

This output of the specs mentions a watcher bucket. This is a class I did create for encapsulating watcher registrations. I chose to create a class in this case because we need extra behavior over the behavior of a standard Array, we want it to be able to stop and start the registered watchers and the map method should return a new WatcherBucket instead of an Array. For brevity, this class and its specs aren't included in the listings in this chapter, but they are provided with the code samples for this book. Listing 4 shows the complete implementation of the builder class.


class WatcherBuilder
  
  include WatcherSyntax                                              #1
  
  def initialize(path, *filters, &configure)                             #2
    @path = path
    @filters = register_filters [], *filters  
    @subdirs = false
    @handlers = {}    
    instance_eval &configure if configure                                 #3
  end
  
  def build
    Watcher.new @path, @filters, @subdirs, @handlers                      #4
  end

  def method_missing(name, *args, &b)                                     #5
    if name.to_s =~ /^on_(.*)/
      self.on $1, *args, &b
    else
      super
    end 
  end
  
  def self.watch(path, *filters, &b)
    @watchers ||= WatcherBucket.new                                       #6
    @watchers << WatcherBuilder.new(path, *filters, &b).build
  end
  
  def self.build(&b)                                                      
    @watchers = WatcherBucket.new
    instance_eval(&b)
    @watchers.start_watching
    @watchers
  end
  
end

#1 Include the Syntax module
#2 Initialize defaults
#3 Call instance_eval with extra config
#4 Build an implementation object
#5 Method missing for named handlers
#6 Entry point for the DSL

Listing 4: The completed builder

The first thing we do is include the WatcherSyntax module in the WatcherBuilder class (#1). This gives us access to all the instance methods defined in that module. Next, we set up a constructor method (#2) that will set up some defaults. The last thing it does is call instance_eval and pass it the &configure block (#3).

Using instance_eval in this case is what gives us clean syntax without defining a receiver for the methods. When you call instance_eval in an object instance, the contents of that block are evaluated with the receiver of self; that is, self becomes the context of the block.

After the initialization of the builder, we defined a build instance method that creates a new instance of a Watcher implementation (#4). The last instance method that is defined is method_missing (#5). You may have noticed that we've only defined the on method in the WatcherSyntax module, which takes an action as first argument. We're going to leverage method_missing as a method dispatcher for all methods that start with on_. The remainder of the method name is assumed to be an action name and it gets dispatched to the on method. If it doesn't start with on_, the call gets passed on to the old behavior.

The last two methods are class methods, and both could serve as entry points into our DSL. The first one defines a single watch on a path with its handlers and adds those to the WatcherBucket (#6) contained by the @watchers singleton variable. The last method build is the actual entry point for our DSL and can handle many calls to the watch, thereby allowing the creation of multiple watches on multiple paths. All that is left to do now for us to get to the syntax shown in Listing 1 is define a global method that forwards its call to the build method of the WatcherBuilder class.

  
def filesystem(&b)
  FsWatcher::WatcherBuilder.build(&b)
end

If we were to run the specs at this point, they should all pass, allowing us to move on to the actual implementation of the Watcher class.


(master)» ibacon -q spec/**/*_spec.rb
  .....................

21 tests, 29 assertions, 0 failures, 0 errors
   

All specs pass, the natural order of the universe is preserved -- in short, all is well with the world. We now have a configuration DSL that can build watchers and start them if only they would have been implemented. The next and last part of this article talks about implementing the actual functionality.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV