Development Blog

 Wednesday, March 28, 2007

Shortly after I posted my original EleutianControllerTests, Scott Bellware, Hamilton and myself chatted about the subject, and Hamilton decided to help out by making some of the previously internal methods public. Context still isn't settable directly however (maybe we should have him change that too...), so you still have to do a little bit of magic to get your context in there. There's actually more code to do this now then when I just set it w/ reflection, but it's arguably more "correct" and doesn't involve "reflection" and you can mock more (like logging and such). Anyways, here's the code:

 

using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.ComponentModel.Design;
using System.Security.Principal;
using System.Web;

using Castle.MonoRail.Framework;
using Castle.MonoRail.Framework.Internal;
using Castle.Core.Logging;
using Castle.MonoRail.Framework.Services;

using NUnit.Framework;

using Rhino.Mocks;

public class EleutianControllerTests
{
  #region Constants
  private const string ApplicationPhysicalPath = "Q:\\PhysicalPath";
  #endregion

  #region Member Data
  protected string _virtualDirectory = String.Empty;
  protected MockRepository _mocks;
  protected IRailsEngineContext _context;
  protected IRequest _request;
  protected IResponse _response;
  protected IServerUtility _serverUtility;
  protected IDictionary _session;
  protected Flash _flash;
  protected IViewEngineManager _viewEngineManager;
  protected ControllerMetaDescriptor _descriptor;
  protected NameValueCollection _parameters;
  #endregion

  #region Test Setup and Teardown Methods
  [SetUp]
  public virtual void Setup()
  {
    _mocks = new MockRepository();
    _viewEngineManager = _mocks.CreateMock<IViewEngineManager>();
    _descriptor = _mocks.CreateMock<ControllerMetaDescriptor>();
    _context = _mocks.CreateMock<MockRailsEngineContext>(_viewEngineManager, _descriptor);
    _request = _mocks.DynamicMock<IRequest>();
    _response = _mocks.DynamicMock<IResponse>();
    _serverUtility = _mocks.DynamicMock<IServerUtility>();
    _session = _mocks.DynamicMock<IDictionary>();
    _flash = new Flash();
    _parameters = new NameValueCollection();
  }

  protected void InitializeController(Controller controller, string areaName, string controllerName, string actionName)
  {
    controller.InitializeControllerState(areaName, controllerName, actionName);
    controller.InitializeFieldsFromServiceProvider(_context);

    InitializeRailsContext(areaName, controllerName, actionName);
  }

  protected void InitializeRailsContext(string areaName, string controllerName, string actionName)
  {
    SetupResult.For(_context.UrlInfo).Return(
      new UrlInfo("eleutian.com", "www", _virtualDirectory, "http", 80,
                  Path.Combine(Path.Combine(areaName, controllerName), actionName), areaName, controllerName,
                  actionName, "rails"));
    SetupResult.For(_context.Server).Return(_serverUtility);
    SetupResult.For(_context.Flash).Return(_flash);
    SetupResult.For(_context.ApplicationPath).Return("/");
    SetupResult.For(_context.Request).Return(_request);
    SetupResult.For(_context.Response).Return(_response);
    SetupResult.For(_context.ApplicationPhysicalPath).Return(ApplicationPhysicalPath);
    SetupResult.For(_request.Params).Return(_parameters);
    SetupResult.For(_context.Session).Return(_session);
  }
  #endregion
}

public abstract class MockRailsEngineContext : IRailsEngineContext
{
  #region Member Data
  private IViewEngineManager _viewEngineManager;
  private IControllerDescriptorProvider _controllerDescriptorProvider;
  #endregion

  #region Properties
  public abstract void Transfer(string path, bool preserveForm);
  public abstract string RequestType { get; }
  public abstract string Url { get; }
  public abstract string UrlReferrer { get; }
  public abstract HttpContext UnderlyingContext { get; }
  public abstract NameValueCollection Params { get; }
  public abstract IDictionary Session { get; }
  public abstract IRequest Request { get; }
  public abstract IResponse Response { get; }
  public abstract ITrace Trace { get; }
  public abstract ICacheProvider Cache { get; }
  public abstract Flash Flash { get; }
  public abstract IPrincipal CurrentUser { get; set; }
  public abstract Exception LastException { get; set; }
  public abstract string ApplicationPath { get; }
  public abstract string ApplicationPhysicalPath { get; }
  public abstract UrlInfo UrlInfo { get; }
  public abstract IServerUtility Server { get; }
  public abstract IDictionary Items { get; }
  public abstract Controller CurrentController { get; set; }
  #endregion


  #region Constructors
  public MockRailsEngineContext(IViewEngineManager viewEngineManager, ControllerMetaDescriptor descriptor)
  {
    _viewEngineManager = viewEngineManager;
    _controllerDescriptorProvider = new ControllerDescriptorProviderStub(descriptor);
  }
  #endregion

  #region Methods
  public abstract void AddService(Type serviceType, object serviceInstance);
  public abstract void AddService(Type serviceType, object serviceInstance, bool promote);
  public abstract void AddService(Type serviceType, ServiceCreatorCallback callback);
  public abstract void AddService(Type serviceType, ServiceCreatorCallback callback, bool promote);
  public abstract void RemoveService(Type serviceType);
  public abstract void RemoveService(Type serviceType, bool promote);
  public object GetService(Type serviceType)
  {
    if (typeof(IViewEngineManager).Equals(serviceType))
    {
      return _viewEngineManager;
    }
    else if (typeof(IControllerDescriptorProvider).Equals(serviceType))
    {
      return _controllerDescriptorProvider;
    }
    else if (typeof(ILoggerFactory).Equals(serviceType))
    {
      return null;
    }
    else if (typeof(IUrlBuilder).Equals(serviceType))
    {
      return new DefaultUrlBuilder();
    }

    return null;
  }
  #endregion

  #region Classes
  private class ControllerDescriptorProviderStub : IControllerDescriptorProvider
  {
    private ControllerMetaDescriptor _descriptor;

    public ControllerDescriptorProviderStub(ControllerMetaDescriptor descriptor)
    {
      _descriptor = descriptor;
    }

    public ControllerMetaDescriptor BuildDescriptor(Controller controller)
    {
      return _descriptor;
    }

    public ControllerMetaDescriptor BuildDescriptor(Type controllerType)
    {
      return _descriptor;
    }

    public void Service(IServiceProvider provider)
    {
    }
  }
  #endregion
}

Source
by Aaron on Wednesday, March 28, 2007 2:51:30 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [6]  |  Trackback

First, a small introduction...

I'm glad to see Ruby has gotten the respect it deserves. I've always felt it was a far prettier language than Python or Perl. While it's still prone to some of the line noise problems of Perl, it's definitely not as bad. I need to say that it's been several years before I've done any serious work with an interpreted language. When I worked at UCR we were almost entirely a PHP/Python/Ruby outfit. In that order, unfortunately. About four and half years ago I did a technical seminar on Ruby. There was a huge push in the department to go to Python and I felt compelled to be the voice of reason. Since my time at UCR I've neglected scripting languages. I feel that one has to be extremely careful when using a scripting language to make a large, enterprise application. I can't stress that enough, careful. I'm sure it's possible, maybe. So yeah, I'm skeptical. But I also encourage people to use the best language for them and the project.

Lately I've been hearing more and more about Ruby and I've been wondering about its applications in the context of someone doing large scale .NET development. My Journey Through The Languages took me through Java before C#. Scenarios I pictured kept reminding me of the Cocoon project and their Flow language. They employ an embedded javascript engine to allow developers to "glue" the backend and the view together. It's an interesting idea. You create your backend, the service layer for the heavy lifting and all that. Then, you let another developer create the features and build the UI using this scripting language. Via their use of continuations (see sendPageAndWait it's neat) they made a lot of the server side programming for the web feel very natural. Oddly enough, the project we used it for moved towards Tapestry. Cocoon is primarily for XML processing, with some other stuff kind of tacked on, that's another blog though.

So, to the point. I started looking back at Ruby and playing with this. I mostly focused on the .NET related stuff in the Ruby community. I looked at a few, but one that really caught my eye was RubyCLR (and this one). So, RubyCLR is a bridge. It's primary goal is to allow calling .NET from Ruby. You can create .NET objects and call methods, properties etc... What it does is generate DynamicMethods (IL at runtime) and return .NET delegates as the Ruby functions. Obviously, there's more to it. It's a pretty elegant solution and very slick. I like stuff that's slick. So, after the weekend, here's what I have:

First we define our MonoRail Controller, it looks like this:

using System;
using MonoRuby.Extension;
    
namespace MonoRuby.Site.Controllers
{
  public class HomeController : MonoRubyController
  {
  }
}

Next, the ruby controller:

class HomeController < Controller
  def index()
    @name = "Jacob Lewallen"
    @today = System::DateTime.Now.ToString()
    render_view 'Index'
  end
end

We can then make our NVelocity (or Brail) view:

<p>
  Hello, $Name! It's $Today...
</p>

Compile, point IIS at it and away you go. Right now it's a very rudimentary implementation. It's got a few quirks, mostly with reloading the web application and how RubyCLR manages its cache of shadow classes and type maps. I did have to dig a little deeper than I'd hoped:

  1. Because RubyCLR is for the opposite "direction", calling .NET from Ruby. I'm embedding Ruby and had to add some stuff that RubyCLR was missing. Mostly bootstrapping code and that sort of thing for starting the Ruby interpreter.
  2. I developed against the RubyCLR trunk because I'm bleeding edge. So, I had to compile RubyCLR and Ruby myself. For some reason, it appeared the Ruby I built used a different calling convention than RubyCLR was developed against. Discovering this ate up most of my time. I attributed the delegate used to pass Ruby managed function pointers and all my weird segfaults went away. I chose this over a /Gz when compiling Ruby.

So here's what's happening:

Application Startup

  1. MyHttpApplication creates a very simple container to store the services and starts them.
  2. DefaultRubyService initializes the RUBYLIB environment variable and then initializes ruby.
  3. DefaultMonoRubyService loads the Ruby sources that we'll be delegating to later (bootstrap.rb, monoruby.rb, and engine.rb)
    1. Ruby sources declare some classes and such we'll be needing and setup IO redirection so I can print stuff to OutputDebugString from Ruby.
    2. We create our MonoRubyGateway (a Ruby implementation of the .NET interface IMonoRubyGateway)
    3. We pass that gateway reference back to the container, so the .NET code can talk to us.
  4. We're done initializing.

Request Handling

  1. [C#]
  2. It all begins when MonoRail calls our MonoRubyController SelectMethod override. Usually SelectMethod determines and returns the reflected MethodInfo for our action. Instead, we save the action name and return the same MethodInfo every time.
  3. MonoRail then calls our InvokeMethod override with the MethodInfo from earlier, which we ignore. We then ask the DefaultMonoRubyService to dispatch the action, giving it a reference to our controller, the action name, and the action arguments.
  4. DefaultMonoRubyService then asks the IRubyService to run a block of code "protected". All this means is that Ruby exceptions won't kill the application. IRubyService will turn them into RubyExceptions and pass them up along with a Ruby stacktrace.
  5. Our MonoRubyGateway from earlier is called, passing the baton to Ruby.
  6. [Ruby] - Lots of RubyCLR Hand Waving
  7. Build the path to the ruby controller sources (home_controller.rb) and load them.
  8. Create a new ActionDispatcher and call it.
  9. We get the class for our controller, create a new instance, and then initialize it:
    1. To prepare, we set the controller reference and other stuff as instance variables.
    2. Action arguments become @params, etc...
  10. We get the method for the action, and call it.
  11. Methods on Ruby's Controller base class call into the C# Controller. (It's actually an adapter, we're passing IController references around, and ControllerAdapter wraps MonoRail's Controller)
    1. render_view does a @controller.render_view
    2. render_text does a @controller.render_text
    3. We can access the PropertyBag via @controller.PropertyBag, which we do in the next step.
  12. Afterwards, we gather up all of our instance variables, and toss them in the property bag.

All done! Request served. I'm curious to know what people think. Obviously, this code is experimental... in fact, use a VM :)  If while playing around you get errors about duplicate types and such from RubyCLR, do an iisreset.

In hindsight, it wasn't necessary to use RubyCLR. When it comes to interoperability, that was the best bet. You can easily do this kind of thing with one of the Ruby .NET based interpreters once their interoperability support is further along. It would certainly be cleaner. I just didn't find one I liked. This just felt very appropriate. Also, I'm sure it's faster. Performance has always been a concern of mine when it came to using Ruby, but there seems to be some work in that area. I also didn't need to use MonoRail, but it made things much more straightforward. This was a side project. I'll probably play around a little more. I'm mostly concerned about what the community thinks. Thumbs up? Down?

Enjoy.... It's been a week since I looked at this source, so if anybody has any trouble building they can IM me or email me.

Source and Binaries

by Jacob on Wednesday, March 28, 2007 9:46:41 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Tuesday, March 27, 2007
using NUnit.Framework;

[TestFixture]
public class OperatorExplicitTests
{
  public interface IFoo { }
  public sealed class Foo : IFoo { }

  public class Bar : IFoo
  {
    private Foo _foo = new Foo();

    public static explicit operator Foo(Bar bar)
    {
      return bar._foo;
    }
  }

  [Test] // Passes
  public void TestCastFromClass()
  {
    Bar bar = new Bar();
    Foo foo = (Foo)bar;
  }

  [Test] // Passes
  public void TestCastFromInterfaceToNormal()
  {
    IFoo bar = new Bar();
    Foo foo = (Foo)(Bar)bar;
  }

  [Test] // Throws System.InvalidCastException: Unable to cast object of type 'Bar' to type 'Foo'.
  public void TestCastFromInterface()
  {
    IFoo bar = new Bar();
    Foo foo = (Foo)bar;
  }

}

Ugh, see what happens when you try to be sneaky? Is this a bug? A limitation?

If you're curious why I care, we have a custom implentation of IDbCommand that wraps a SqlCommand and this code expects it to be a SqlCommand... no fooling it I guess :/

by Aaron on Tuesday, March 27, 2007 11:15:48 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [1]  |  Trackback

Update: Hamilton committed my patch, so if you're running off trunk you won't need to do this anymore. Thanks Hamilton!

Currently MonoRail uses ResourceSets wrapped up in an implementation of IResource to provide Resources to views. Unfortunately, ResourceSets do not cascade when it comes to resource resolution.  ResourceManager.GetResourceSet does have a flag called tryParents, but all that does is try less and less specific cultures until it finds a match. In other words, if you have two resource files:
Foo.ko.resx: defines A, B
Foo.resx: defines A, B, C

If you locale is ko-KR, calling RseourceManager.GetResourceSet will yield a ResourceSet that maps to Foo.ko.resx, so asking for B will work, but asking for C will not. A ResourceSet only knows about itself.

ResourceManagers on the other hand, are perfectly capable of handling this cascade, which is quite necessary in at least our localized app, as we do not want to have to define strings in all languages for everything. So with ResourceManagers, asking for A, B, and C all behave as expected, preferring the most specified culture and cascading down as necessary.

Below is an implementation of an IResourceFactory that spits out wrapped ResourceManagers instead of ResourceSets.

To use it you'll need to add this to your web.config:

<monorail>
  <services>
    <service id="ResourceFactory" type="Eleutian.Shared.MonoRail.ResourceManagerFactory, Eleutian.Shared" />
  </services>
</monorail>

Source

by Aaron on Tuesday, March 27, 2007 4:13:12 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, March 24, 2007

If you're reading this, you're probably interested in some of the stuff we're doing, which means we're probably interested in you. We're currently looking for Software Developers and Testers in Seattle. If you'd like to join our team, drop us an email with your resume to jobs@eleutian.com.

by Aaron on Saturday, March 24, 2007 4:11:15 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback

I'm sure you've all heard that the best developers are lazy and/or dumb. I certainly agree with that. Also, you know that writing repetitive code and repeatedly following tedious steps is bad and a waste of time.

OK, so we know that those things are bad... but how do you identify them? Often tedious steps and repetitive code is taught as the way you do things... so you just do them in that way without asking questions. Other times, we just don't know that there is an easier way. We're developers--we're creative, smart, inventive, and we should be able to tell when something can and should be done in a better way. 

Let me give you an example of some code I came across some time ago:

Public userstring As String = "U000000"

Private Function GetUserIDStr(ByVal userid As Integer) As String
  'Returns String:
  '   User Info FileName
  Dim source As String = userid.ToString()
  'Dim destination As Char() = {"U"c, "0"c, "0"c, "0"c, "0"c, "0"c, "0"c}
  Dim destination As String = Me.userstring
  Dim sourcelen As Integer = source.Length()
  Dim destindex As Integer = 7 - sourcelen
  source.CopyTo(0, destination, destindex, sourcelen)
  Return destination
End Function

Obviously there are several things... off... with this code. Let's discuss some of them. The first is the original Dim destination that is now commented out. This is an example of someone partially applying what I'm talking about. They realized that typing out that array was unnecessarily complicated so they did something about it. That's great, that's what we need to do. Unfortunately, they stopped there. I don't think we should stop there. We should look at the code, say, wow, this isn't really doing anything but formatting a number into a string. There has to be a better way to do this! In this case there certainly is... all of that code can be replaced with something along the lines of:

Return String.Format('U{0:D06}', userid)

Which is easier to read? Write? Maintain? The answer is obvious... even if you didn't know about String.Format, you should know when you're writing that code that there has to be a better way. Google is your friend.

What if there isn't a better way? Can you think of a better way? Ask yourself these questions:

  • Are you going to be doing this more than a few times?
  • Are other people on your team going to be doing it?
  • Are you looking for a break from your normal work?

If the answer to any of those questions is yes, take some time to write or come up with a better way. You're a developer. You develop. Remember, you can write software for yourself and your teams that will make writing software more enjoyable and faster. And then, when you're done, share it! Share it with the world, make development easier for everyone.

I'll give you another example. I've already blogged about it, but it's relevant to this post so I'll rehash here. In order to localize a string in MonoRail you must do the following:

  1. Create a new resx file.
  2. Add a Resource attribute to your controller mapping the resx to a key in the PropertyBag.
  3. For each string you want to localize, add a key/value pair to the resx file.
  4. Replace the strings in your view template with your resource key.

Hm. Every controller? Every string? Talk about context switching. Not to mention the fact that you can't see the actual English string in your view template, so any modifications to the English string require the same context switching. It didn't take me long to decide that was far too tedious for myself and my team.

It probably took me 2-3 days overall to write both the ASP.NET preprocessor and adapt it to MonoRail/Brail. Will I ever get 2-3 whole days back? Who knows. Will my entire team in total? More likely. Does it save us countless context switches, speed up our development, and make localization trivial? Absolutely. So was it worth it? Absolutely.

And before you ask, yes I plan on taking my own advice and sharing it... eventually. Oh and keep in mind, this sort of thinking can and should be applied to everything you do, including but not limited to code, tools, and process.

by Aaron on Saturday, March 24, 2007 3:46:12 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, March 22, 2007

In a previous post I talked about how we use automatically generated interfaces to wrap our PropertyBag's. You can also use this technique in your ViewComponents to clean up that code quite a lot. We used to have code like this:

public class SomeComponent : ViewComponent
{
  public override void Initialize()
  {
    base.Initialize();
    User user = (User)this.ComponentParams["User"];
    float value = this.Service.CalculateThatValue(user);
    this.ComponentParams["SomeCalculatedValue"] = value;
  }
}

Now we can make an interface:

public interface ISomeComponentView
{
  User User { get; set; }
  float SomeCalculatedValue { get; set; }
}

And ask the code we wrote for the PropertyBag to give us a hand in cleaning things up:

public class SomeComponent : ViewComponent
{
  public override void Initialize()
  {
    base.Initialize();
    ISomeComponentView view = this.ViewFactory.ResolveView<ISomeComponentView>(this.ComponentParams);
    float value = this.Service.CalculateThatValue(view.User);
    view.SomeCalculatedValue = value;
  }
}

Much better! If you're looking for an implementation of that ViewFactory service, Lee Henson tossed one together shortly after our post. He's also added some other, pretty nitfy features since then. Like the ability to specify a prefix with an attribute for the generated dictionary keys. As he mentions in his post, you can use it for anything (and probably should) that uses strings as keys into an IDictionary (Session, Flash, etc..). I'm planning on checking it in to CastleContrib soon, until then, grab it off the list. Thanks Lee!

by Jacob on Thursday, March 22, 2007 9:21:27 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 16, 2007

Last week, Aaron and I were hanging out with a bunch of the Microsoft MVP's that were up in Seattle for the MVP Summit. We had an absolute blast and got to meet some excellent developers. It was a great opportunity to get a sense for how others in our situation were handling similar problems and to share things that we have all learned along the way. We had a few beers at the Party with Palermo event and the following day a few of the guys dropped by the Eleutian Seattle offices and we threw together a podcast with Scott Hanselman. It's a very quick introduction to MonoRail for those that aren't familiar. We go on to discuss testability and IoC in the context of the Castle project. It was a great chance to expose MonoRail and the concepts behind it to a wider audience.

In no particular order, here are some blogs if you're interested in some good reading on various areas of software development:

  • Scott Bellware - Development practices, C# 3.0, Ruby, and more. His company, Dovetail, is looking into starting development using MonoRail. They are hiring.
  • Jeremy Miller - Agile, especially TDD, IoC, etc...  Jeremy wrote StructureMap, an IoC framework. So, he definitely knows the why behind a lot of the agile practices.
  • Scott Hanselman - Anything and everything development and technology. I always find something new here. I'm sure you will also.

I'm glad to see some of the news that came out of the summit. I'm sure one of us will be back to expound on them once we've done a little more research and tinkering.

by Jacob on Friday, March 16, 2007 10:56:57 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, March 01, 2007

I needed the ability to execute a task from an MsBuild project and have MsBuild keep going and not wait around on the process to exit. I was hoping to find an option on the Exec task for this but that search turned up nothing. After scowering a bit I couldn't find one somebody else wrote, so here we go:

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;

using Microsoft.Build.Tasks;

namespace AsyncExec
{
  public class AsyncExec : Exec
  {
    #region Task Members
    protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands)
    {
      Process process = new Process();
      process.StartInfo = GetProcessStartInfo(pathToTool, commandLineCommands);
      process.Start();
      return 0;
    }
    #endregion

    #region Methods
    protected virtual ProcessStartInfo GetProcessStartInfo(string executable, string arguments)
    {
      if (arguments.Length > 0x7d00)
      {
        this.Log.LogWarningWithCodeFromResources("ToolTask.CommandTooLong", new object[] { base.GetType().Name });
      }
      ProcessStartInfo startInfo = new ProcessStartInfo(executable, arguments);
      startInfo.WindowStyle = ProcessWindowStyle.Hidden;
      startInfo.CreateNoWindow = true;
      startInfo.UseShellExecute = true;
      string workingDirectory = this.GetWorkingDirectory();
      if (workingDirectory != null)
      {
        startInfo.WorkingDirectory = workingDirectory;
      }
      StringDictionary environmentOverride = this.EnvironmentOverride;
      if (environmentOverride != null)
      {
        foreach (DictionaryEntry entry in environmentOverride)
        {
          startInfo.EnvironmentVariables.Remove(entry.Key.ToString());
          startInfo.EnvironmentVariables.Add(entry.Key.ToString(), entry.Value.ToString());
        }
      }
      return startInfo;
    }
    #endregion
  }
}

Don't fret over the WindowStyle and CreateNoWindow flags, Exec works by spawning cmd.exe with a batch file that it generates with our command. Without those flags, the command prompt window flashes by.

Obviously, very rough stuff and I make no promises of it's abilities. It worked for the simple example below and that's all I wanted. Ideally, the task would ensure the executable being started exists so we could at least fail if that's the case. But, for now this works for us. It's only used in interactive situations to spawn NCoverExplorer and things like thing. We can already easily spawn the HTML reports by using:

<Exec Command="$(ArtifactsDirectory)\mbunit-tests.html" />

But that just doesn't work when you need to run an application and not care about when it's closed:

<AsyncExec Command="$(ToolsDirectory)\NCoverExplorer\NCoverExplorer.exe $(ArtifactsDirectory)\coverage.xml" />

So here's the binaries in case somebody else is curious as well as the UsingTask.

<UsingTask AssemblyFile="AsyncExec.dll" TaskName="AsyncExec.AsyncExec" />

Obviously, if something like this is already easily possible and this was a half hour wasted, let me know. Especially if it's possible "out-of-the-box". I'm always glad to have one less dependency.

Source and Binaries

source | tools | msbuild
by Jacob on Thursday, March 01, 2007 12:52:18 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [1]  |  Trackback