Tuesday, October 31, 2006

Though I knew you could do it, I never have till today... and that's supplying a type for a generic parameter in another type, which is registered in a container... Of course I didn't have to do it that way, but it it kept the configuration a little thinner.

Basically I have some simple WCF services I wanted to host in a container.. here's the service's interface...

[ServiceContract(Namespace="http://localhost/schemas/testrunner/", SessionMode=SessionMode.Required)]

public interface ITestRunner

{

    [OperationContract]

    Guid StartTestRun(Guid testSuiteId);

 

    [OperationContract]

    TestRunStatus GetRunStatus(Guid testRunId);

}

To get this to work for my implementation I had to do one thing, which was set the InstanceContextMode to "single" for the service implementations behaviour, otherwise the service host would die when I tried to pass in an instance (it expects a type for any other mode)... I haven't dug very deep into WCF, but it would be nice if they supported a mechanism for supplying a component activator instead...

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]

public class TestRunner : ITestRunner

{

Now, I build a component for hosting my service... it implements IStartable...

public class HostingWrapper<T> : IStartable

    where T: class

{

    private ServiceHost _host;

    private T _service;

    private ILogger _log;

 

    public HostingWrapper(ILogger log, T service)

    {

        if (log == null) throw new ArgumentNullException("log");

        if (service == null) throw new ArgumentNullException("service");

 

        _log = log;

        _service = service;       

    }

 

    public void Start()

    {

        _host = new ServiceHost(_service);

        if (_log.IsDebugEnabled) _log.Debug("Opening ServiceHost for service: {0}", _service.GetType());

        _host.Open();

        if (_log.IsInfoEnabled) _log.Info("Opened ServiceHost for service: {0}", _service.GetType());

    }

 

    public void Stop()

    {

        if (_log.IsDebugEnabled) _log.Debug("Closing ServiceHost for service: {0}", _service.GetType());

        _host.Close();

        ((IDisposable)_host).Dispose();

        if (_log.IsInfoEnabled) _log.Info("Closed ServiceHost for service: {0}", _service.GetType());

        _host = null;

    }

}

And then you just need to regsiter it in the container's configuration:

<components>

    <component id="testRunnerService.default"

        service="BoatsForGoats.Services.Testing.ITestRunner, BoatsForGoats.Services"

        type="BoatsForGoats.Services.Testing.TestRunner, BoatsForGoats.Services" />

 

    <component id="testRunnerHost.default"

        type="BoatsForGoats.Services.HostingWrapper`1[[BoatsForGoats.Services.Testing.ITestRunner, BoatsForGoats.Services]], BoatsForGoats.Services" />

About the only tricky thing is that I guessed (incorrectly) that I would only need a single set of square brackets around the generic parameter.

 |  | 
posted @ Monday, October 30, 2006 11:21:10 PM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |
 Friday, October 27, 2006

Well I blew up my feedburner yesterday... my RSS feed was set to max out at 50 items, which, with my love for pasting html-bloated source code was well over the 256K limit.... but feedburner never bothered tell me it was dead... only noticed today... at any rate, I tried resetting the size to a max of 6 posts in the feed... and pinging it... but it was still dead, so I did a resync... still no joy... In the end I found that I had to turn off feed burner support in das blog and then resync the feed.

Moral of the story, don't bloat your feed (It was bad form at any rate, especially in bandwidth starved New Zealand ;o)

If you can see this post it's all working again happily...



posted @ Friday, October 27, 2006 2:52:24 AM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |
 Thursday, October 26, 2006

Well lots to blog about, but no time to do it... at least not today - at any rate I just thought I would say if you haven't considered using Base4 up until now, why not give it a go with the latest drop :) I'm loving these new changes with the support for Compile-time query handling... I think at this point you can start prototyping stuff with base4 very quickly, between the UI for designing or discovering an existing schema, and now the ability to avoid learning the ObjectPath query syntax it's pretty compelling...

At any rate, as mentioned in the various posts from Alex James, you can now write these kinds of queries:

IItemList<FileBase> matchingFiles = StorageContext.Find<FileBase>(FileBase.Fields.FileSize > 1024000 && FileBase.Fields.MimeType == "audio/wav");


Faboo, and of course you can drill through the relationships:

Track track =  StorageContext.FindOne<Track>(Track.Fields.Name == reference.TrackName

    && Track.Fields.Release.Name == reference.ReleaseName

    && Track.Fields.Release.Artist.Name == reference.ArtistName);


This is for a simple hierarchy, where we have Artists-> Releases -> Tracks (sadly organising Music in reality isn't quite this easy...)

But as you can see these queries are getting quite verbose, personally this is where the elegance of this approach comes in... reuse is possible, for example - we can build a static class to hold common queries:

public static class FileBaseQueries

{

    public static ExpressionField LargerThenOneMegabyte

    {

        get

        {

            return FileBase.Fields.FileSize > 1024000;

        }

    }

 

    public static ExpressionField MimeTypeIsAudioWav

    {

        get

        {

            return FileBase.Fields.MimeType == "audio/wav";

        }

    }

}


And now the first query could be rewritten:

StorageContext.Find<FileBase>(FileBaseQueries.LargerThenOneMegabyte && FileBaseQueries.MimeTypeIsAudioWav);


The main thing I like about this approach is your queries start looking like sentences... "Larger Then One Megabyte and Mime Type Is Audio Wav"...

Another idea might be to use a method on the static class instead...

public static ExpressionField MimeTypeIs(string type, string subType)

{

    return FileBase.Fields.MimeType == string.Format("{0}/{1}", type, subType);

}


And then writing something like:

StorageContext.Find<FileBase>(FileBaseQueries.LargerThenOneMegabyte && FileBaseQueries.MimeTypeIs("audio","wav"));


At this point it still reads quite nicely, but it's a little more flexible...

Edit: I just noticed this entry wasn't actually marked as syndicated... fixed it now.
posted @ Wednesday, October 25, 2006 7:36:06 PM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |
 Thursday, October 19, 2006

I think there's 3 levels of skill involved with the Castle IoC implementation... first off you get a handle of the XML configuration, registering components, using existing facilities... pretty much getting away largely with cut 'n paste coding.

Then you get to the intermediate level.. writing basic facilities, tweaks to the component model, writing your own sub dependency resolvers or component activator, having a go with binsor configuration.

I think the third level is reserved for the Castle team alone... ;o)

This post is going to be straddling the beginner to intermediate kinda level... which is generally all I ever reach with Castle's IoC... it's not often you have to dig deeper day-to-day... though it's always good to know there is a lot of untapped potential there.

So.. for today, say you have a component, like the Base4Host, which has some explicit constructors:

public Base4Host(string appName, int port)

{

    if (string.IsNullOrEmpty(appName)) throw new ArgumentNullException("appName");

    if (port <= 1024) throw new ArgumentOutOfRangeException("port", "port should be greater then 1024");

    _appName = appName;

    _port = port;

}

 

public Base4Host(string appName, int port, string root)

    : this(appName, port)

{

    if (string.IsNullOrEmpty(root)) throw new ArgumentNullException("root");           

    _root = root;

}


You can register it the container easy enough, and provide values for them in XML configuration, but what if you want to do the same programatically... generally your first stop would be to examine the IWindsorContainer for a suitable overload... alas it doesn't get us far, so we dig in to the underlying IKernel itself... the kernel exposes some possible candidates:

void AddComponentWithExtendedProperties(String key, Type classType, IDictionary extendedProperties);

void AddComponentWithExtendedProperties(String key, Type serviceType, Type classType, IDictionary extendedProperties);


So you experiment with them, but supplying the dictionary of extended properties does nothing... hmm... time to file a bug report? well no... extended properties having nothing to do with satisfying parameter or property dependencies on your component - not directly at least.

So why don't we just create the component ourselves.. and then add it to the container?

Well you can, via the Kernel.AddComponentInstance method but you're going to miss out on some things... for instance the startable facility won't be "concerned" with your component, and as such if it implements IStartable it won't get started and stopped... Though I haven't confirmed this, I dont think the container will bother to dispose of any IDisposable components registered in this fashion either... the container doesn't consider itself the owner of the component (and generally this is what we want).

So we're going to have to get a little more intimate with the container implementation ... so every time a component is registered in the container a corresponding ComponentModel is generated for the component, this basically keeps track of the:
  • Components dependencies
  • Constructor candidates
  • Parameters (sounds like us...)
  • Name, implementation type and service type.
  • Lifecycle, Lifestyle...
  • And some other stuff you can discover for yourself.
Now we could get heavy handed and jump into contributing to the construction of the component model itself... but it's pretty uncessary, we just want to tweak the end result... so we can use an event handler on the Kernel - ComponentModelCreated.

So here we have an implementation that solves our problems... This is being implemented inside a facility, but you could do this anywhere... wire it up in your custom container that's derived from WindsorContainer maybe, obviously you want to remove the if statement for checking the Implemenation type is Base4Host though. :)

private const string AdditionalParametersKey = "AdditionalParameters";

 

private void Kernel_ComponentModelCreated(Castle.Core.ComponentModel model)

{

    if (model.Implementation == typeof(Base4Host))

    {

        if ((model.Configuration == null)

            && model.ExtendedProperties.Contains(AdditionalParametersKey))

        {

            Dictionary<string, object> additionalParameters = (Dictionary<string, object>)model.ExtendedProperties[AdditionalParametersKey];

            foreach (string parameterName in additionalParameters.Keys)

            {

                model.Parameters.Add(parameterName, Convert.ToString(additionalParameters[parameterName]));

            }

        }

    }

}

 

protected override void Init()

{

    Kernel.ComponentModelCreated += new ComponentModelDelegate(Kernel_ComponentModelCreated);

}


Now I can register my component by doing something like this:

Dictionary<string, object> additionalParameters = new Dictionary<string, object>();

additionalParameters.Add("appName", _applicationName);

additionalParameters.Add("port", _port);

additionalParameters.Add("root", _baseDirectory);

 

Hashtable properties = new Hashtable();

properties.Add(AdditionalParametersKey, additionalParameters);

 

Kernel.AddComponentWithProperties("base4.defaultHost", typeof(Base4Host), properties);


Here we're passing all our additional parameters as a Dictionary<string, object> inside our IDictionary of additional properties....  this isn't the most elegant implementation, but this is all code internal to a single facility so it's not really important to me... and it gets the job done just fine.


posted @ Wednesday, October 18, 2006 8:46:17 PM (New Zealand Daylight Time, UTC+13:00)    Comments [2] | Trackback |
 Tuesday, October 17, 2006

Abstractions...  mmmm... so sweet

...a post from Ayende on abstractions, and specifically a mention of the logging "abstraction" in Castle... it's an interesting thing, for the Seismic product I worked logging (via the same abstraction) into a product a year or more ago...  in the end we needed the ability to log contextual information (basically exposing features already existing in log4net at the time, but not available via the Castle ILogger interface) and so I ended up creating a new interface that was a superset of the existing abstraction, IExtendedLogger...

here's the devil... looks simple...

public interface IExtendedLogger : ILogger

{

    void AddProperty(string key, object value);

    void RemoveProperty(string key);

}


However... because we're being a good little abstraction "whore" we end up with a few more classes to support this new interface, and make the user experience more pleasurable:



Though I was reasonably pleased with the end result a year ago, that was probably a couple of hours work + some more time tweaking (once you include the time to code up that test fixtures) that would've been better spent building additional functionality into the product.

I have to admit the lure of a needless abstraction is ever present to me... I enjoy halving a class into an abstract and concrete implementation, and then extracting an interface is like the candy coating... prefixing the concrete implementation with "Default" makes it even seem like you've all but got people lining up to create their own versions!  Wow, isn't it powerful, flexible.. and all sorts of other words ending in "ul" or "ile".

....But in the end, needless is needless - and I haven't needed the flexibility gained from this abstraction so far... after a year... a whole year, that's pretty much like never... sure I had grand plans, but they never did come to fruition... and grand plans don't keep me fed - YAGNI strikes again.

About the only thing I find the needless abstractions do is in clarifying my thinking on what I do and don't need, or more importantly what I do and don't want people to do with my code.... it let's me define just what I want my "pit of success" behavior to be, albeit not the implementation... Perhaps some of this stuff is better reserved for throw away prototypes then production code.

I think the best book I've found for discouraging this "80's guitar solo" of abstraction..um...ism is the Framework Design Guidelines... Though these abstractions do make it easy for me to maintain and grow my code (and the orthogonality of the design is generally good) it comes at the price of other people having difficulty learning my API through experimentation, and I fail to create a progressive framework... which after reading the aforementioned book, is something we all wan't to do.

I guess the final question is, do you reverse an abstraction that isn't required after such a long time...?  Or do you just avoid making the same mistake twice and live with the abstraction, assuming it's not hurting too many people - Is it worth lumping it with the rest of the "broken windows" in a project, or is that a little too brash?

posted @ Tuesday, October 17, 2006 9:53:52 AM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |

Nothing to do with code, but how many people are or have watched time trumpet in NZ?
posted @ Tuesday, October 17, 2006 9:46:08 AM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |

Startable facility and hosting base4

Ivan's pretty keen :) last week he demonstrated hosting the base4 service in ASP.Net ... cool stuff.

Now here he has placed the implementation in the HttpApplication class, and as a quick observation this would be a good candidate for moving into it's own component... and as luck would have it, Castle has just the thing, via the Startable facility.

Startable... (and the implied, stoppable ;o)

Now the startable facility is a very simple beast, basically... you create a class, that implements this interface:

/// <summary>

/// Interface for components that wish to be started by the container

/// </summary>

public interface IStartable

{

    void Start();

    void Stop();

}


Which lives in the Castle.Core assembly... though the facility itself lives in the Castle.MicroKernel assembly... you should already have references to both, otherwise I doubt your container is working at all :P

And you then register the startable facility in the container:

<facility id="startable.facility"

    type="Castle.Facilities.Startable.StartableFacility, Castle.MicroKernel" />


And thats it... now whenever you register a component in the container, the startable facility will check it, and if it's found to implement IStartable it will add it to a list of components "awaiting startup"... once any of these awaiting components has had all it's dependencies satisfied it will be started (so it may be started as soon as the component is registered, if it doesn't have any dependencies - this will be the case for our component)...

Disposing of the container will cause the components to be stopped in turn... perfect for hosting our base4 server...

Reworking the hosting code to be a startable component...

...so taking what Ivan's done, we could rework it a little... and be able to host base4 in web apps, as well as say your business logic's test harness... here's my quick 'n dirty reworked implementation as a component that I did this afternoon... my final version probably won't have any dependencies on the HttpContext as I'll just rely on explicit declaration of all the settings as parameters in the container configuration... I tend to like keeping all the config in the container, one place to look 'n all that.

public class Base4Host : IStartable

{

    private const string _machineName = "localhost";       

    private const string _provider = "SQL2005";

    private string _appName;       

    private int _port;

    private string _root;

    private ServerConfiguration _configuration;

    private IServerProxy _proxy;

 

    public ServerConfiguration Configuration

    {

        get { return _configuration; }

    }

 

    public IServerProxy ServerProxy

    {

        get { return _proxy; }

    }

 

    public Base4Host(string appName, int port)

    {

        if (string.IsNullOrEmpty(appName)) throw new ArgumentNullException("appName");

        if (port <= 1024) throw new ArgumentOutOfRangeException("port", "port should be greater then 1024");

        _appName = appName;

        _port = port;

    }

 

    public Base4Host(string appName, int port, string root)

        : this(appName, port)

    {

        if (string.IsNullOrEmpty(root)) throw new ArgumentNullException("root");           

        _root = root;

    }

 

    #region IStartable Members

 

    public void Start()

    {

        _configuration = CreateConfiguration();      

        _proxy = ServerFactory.StartServer(_configuration, false);

        string base4Context = string.Format("tcp://Server:@localhost:{0}/{1}", _port, _appName);

        StorageContext.SetDefault(base4Context);           

    }

 

    public void Stop()

    {

        _proxy.Stop();

        _proxy = null;

        _configuration = null;

    }

 

    #endregion

 

    #region Support methods

 

    private ServerConfiguration CreateConfiguration()

    {

        ServerConfiguration configuration = new ServerConfiguration();

 

        DiscoverApplicationName();

        InsertApplicationRoot(configuration);

        InsertConnectionStrings(configuration);

        InsertConnectivityInformation(configuration);

 

        if (!Directory.Exists(configuration.Store.Root))

        {

            Directory.CreateDirectory(configuration.Store.Root);

        }

 

        return configuration;

    }

 

    private void InsertConnectivityInformation(ServerConfiguration configuration)

    {

        configuration.Store.Name = _appName;

        configuration.Store.Provider = _provider;

        configuration.Store.Port = _port;

        configuration.Store.MachineName = _machineName;

    }

 

    private void DiscoverApplicationName()

    {

        if (string.IsNullOrEmpty(_appName))

        {

            if (HttpContext.Current.Application["AppName"] == null)

            {

                Assembly assembly = Assembly.GetCallingAssembly();

                HttpContext.Current.Application["AppName"] = (AssemblyTitleAttribute.GetCustomAttribute(assembly, typeof(AssemblyTitleAttribute)) as AssemblyTitleAttribute).Title;

            }

 

            _appName = HttpContext.Current.Application["AppName"].ToString();

        }

    }

 

    private void InsertApplicationRoot(ServerConfiguration configuration)

    {

        string root = string.IsNullOrEmpty(_root) ? HttpContext.Current.Server.MapPath("~/") : _root;

        configuration.Store.Root = root.EndsWith("\\") ? root + "App_Data\\Base4" : root + "\\App_Data\\Base4";

    }

 

    private void InsertConnectionStrings(ServerConfiguration configuration)

    {

        string connectionStringName = ConfigurationManager.AppSettings["DefaultConnection"];

 

        if (string.IsNullOrEmpty(connectionStringName) && ConfigurationManager.ConnectionStrings != null

            && ConfigurationManager.ConnectionStrings.Count > 0)

        {

            connectionStringName = ConfigurationManager.ConnectionStrings[0].Name;

        }

 

        configuration.Store.ConnectionString = string.IsNullOrEmpty(connectionStringName) ? ConfigurationManager.AppSettings["Store.ConnectionString"] :

            ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;

 

        SqlConnectionStringBuilder connStrBuilder = new SqlConnectionStringBuilder(configuration.Store.ConnectionString);

 

        connStrBuilder.InitialCatalog = "master";

        configuration.MasterConnectionString = connStrBuilder.ToString();

    }

 

    #endregion

}


Notice we don't expose a default constructor... how can the container create it?

Well, it can't unless we supply the parameters it requires... which is just what we'll do:

<component

    id="base4host.default"

    type="MyProject.Base4Host, MyProject">

    <parameters>

        <appName>GoatsAndBoats</appName>

        <port>11888</port>

    </parameters>           

</component>


Simple... a couple of useful extensions to our existing base4 facility may well be to:
  • Ensure the startable facility is installed... so we can fail-early and with a meaningful error during application startup.
  • Add some additional "optional" parameters for configuring the facility, so that it can take care of registering the Base4Host for us.

For the optional parameters, I'm thinking of doing something like this:

<facility id="base4.storageFacility"

    type="SomeProject.Core.Base4StorageFacility, SomeProject.Core"

    host="true"

    appName="GoatsAndBoats"

    port="11888"

    connectionString="local"

    />


At which point it's trivial to shift between accessing base4.net as a remote service vs. hosting it in your own app... unlike the remote setup you can skip providing the base4 url (context string) as it'll be implied from the app name and port.
 |  | 
posted @ Tuesday, October 17, 2006 12:28:44 AM (New Zealand Daylight Time, UTC+13:00)    Comments [1] | Trackback |
 Monday, October 16, 2006

Trac

I know it's not .Net, but that's not always a bad thing...

Well I tend to keep my eye out for things which take my fancy, or more importantly, free things that take my fancy...

Trac is one of those things, a project management and bug database web app written in Python, written by edgewall software... it's actually pretty cool... for RoR enthusiasts they're probably aware of it's existence already (considering RoR use this for manging their bugs and patches)

First off, you can run it on windows...  though a *nix is generally better, and it's not too hard to get going - took me about half an hour... second thing, it's a lot better once you install a few hacks especially:
Now, at this point what makes it good?
  • SVN Integration
  • Wiki Integration
  • Extensibility and community support
Basically I can check in code with comments containing wiki markup and references to existing "tickets" logged in the system... The wiki integration incidentally is my primary motivation for looking so closely at using this for some of the projects I've been working on... I'm a liability to Seismic Technologies... documenting build processes etc. in a wiki are one way we can minimize that... We already have a customer-focused wiki, but it doesn't encourage maintenance on a day-to-day basis like this product does.

secondary to that is the very good subversion integration, a must considering that's the only source control system I use.

Creating tickets via XML-RPC in C#

Now the last hack I suggested exposes plenty of functionality in the system via XML RPC... at which point we can start logging bugs remotely, from C#... to do this we'll need a copy of the xmlrpc.net library

I'm not conducting a tutorial here ;o) but if Trac takes your fancy you might find this code handy... first off this is our interface for logging tickets in Trac - incidentally this is only a small subset of the funcionality exposed.

[XmlRpcUrl("http://localhost/trac/login/xmlrpc")]

public interface ITicket : IXmlRpcProxy

{       

    [XmlRpcMethod("ticket.query")]

    int[] Query(string qstr);

 

    [XmlRpcMethod("ticket.getRecentChanges")]

    int[] GetRecentChanges(DateTime since);

 

    [XmlRpcMethod("ticket.getAvailableActions")]

    string[] GetAvailableActions(int id);

 

    [XmlRpcMethod("ticket.getTicketFields")]

    TicketField[] GetTicketFields();

 

    [XmlRpcMethod("ticket.create")]

    int Create(string summary, string description, XmlRpcStruct attributes);

 

    [XmlRpcMethod("ticket.get")]

    object[] GetTicket(int id);

 

    [XmlRpcMethod("ticket.delete")]

    void Delete(int id);

 

    [XmlRpcMethod("ticket.update")]

    object[] Update(int id, string comment, XmlRpcStruct attributes);

 

    [XmlRpcMethod("ticket.type.getAll")]

    string[] GetAllTypes();

 

    [XmlRpcMethod("ticket.resolution.getAll")]

    string[] GetAllResolutions();

 

    [XmlRpcMethod("ticket.priority.getAll")]

    string[] GetAllPriorities();

 

    [XmlRpcMethod("ticket.component.getAll")]

    string[] GetAllComponents();

 

    [XmlRpcMethod("ticket.version.getAll")]

    string[] GetAllVersions();

 

    [XmlRpcMethod("ticket.severity.getAll")]

    string[] GetAllSeverities();

 

    [XmlRpcMethod("ticket.milestone.getAll")]

    string[] GetAllMilestones();

}


Now, tickets have a bunch of attributes associated with them...

public static class TicketAttributes

{

    public const string Cc = "cc";

    public const string Keywords = "keywords";

    public const string Status = "status";

    public const string Type = "type";

    public const string Owner = "owner";

    public const string Version = "version";

    public const string Resolution = "resolution";

    public const string Reporter = "reporter";

    public const string Milestone = "milestone";

    public const string Component = "component";

    public const string Summary = "summary";

    public const string Description = "description";

    public const string Priority = "priority";

}


And we can make ticket information easier to get a hold of with a simple class:

/// <summary>

/// represents the information for a ticket

/// </summary>

public class TicketInfo

{

    private int _ticketId;

    private DateTime _created;

    private DateTime _lastModified;

    private XmlRpcStruct _attributes;

 

    public TicketInfo()

    {

    }

 

    public TicketInfo(object[] values)

    {

        Update(values);

    }

 

    internal void Update(object[] values)

    {

        if (values == null) throw new ArgumentNullException("values");

        if (values.Length != 4) throw new ArgumentException("values should have 4 elements");

 

        _ticketId = (int)values[0];

        _created = DateHelper.ParseUnixTimestamp((int)values[1]);

        _lastModified = DateHelper.ParseUnixTimestamp((int)values[2]);

        _attributes = (XmlRpcStruct)values[3];

    }

 

    /// <summary>

    /// The identifier for this ticket

    /// </summary>

    public int TicketId

    {

        get { return _ticketId; }

        set { _ticketId = value; }

    }

 

    /// <summary>

    /// date and time the ticket was created

    /// </summary>

    public DateTime Created

    {

        get { return _created; }

        set { _created = value; }

    }

 

    /// <summary>

    /// date and time the ticket was last modified

    /// </summary>

    public DateTime LastModified

    {

        get { return _lastModified; }

        set { _lastModified = value; }

    }

 

    /// <summary>

    /// The attributes for this ticket, this will include any additional fields

    /// that aren't defined explicitly as members of this class.

    /// </summary>

    public XmlRpcStruct Attributes

    {

        get

        {

            if (_attributes == null) _attributes = new XmlRpcStruct();

            return _attributes;

        }

        set { _attributes = value; }

    }

 

    public string Cc

    {

        get { return GetAttribute(TicketAttributes.Cc); }

        set { SetAttribute(TicketAttributes.Cc, value); }

    }

 

    public string Keywords

    {

        get { return GetAttribute(TicketAttributes.Keywords); }

        set { SetAttribute(TicketAttributes.Keywords, value); }

    }

 

    public string Status

    {

        get { return GetAttribute(TicketAttributes.Status); }

        set { SetAttribute(TicketAttributes.Status, value); }

    }

 

    public string Type

    {

        get { return GetAttribute(TicketAttributes.Type); }

        set { SetAttribute(TicketAttributes.Type, value); }

    }

 

    public string Owner

    {

        get { return GetAttribute(TicketAttributes.Owner); }

        set { SetAttribute(TicketAttributes.Owner, value); }

    }

 

    public string Version

    {

        get { return GetAttribute(TicketAttributes.Version); }

        set { SetAttribute(TicketAttributes.Version, value); }

    }

 

    public string Resolution       

    {

        get { return GetAttribute(TicketAttributes.Resolution); }

        set { SetAttribute(TicketAttributes.Resolution, value); }

    }

 

    public string Reporter       

    {

        get { return GetAttribute(TicketAttributes.Reporter); }

        set { SetAttribute(TicketAttributes.Reporter, value); }

    }

 

    public string Milestone

    {

        get { return GetAttribute(TicketAttributes.Milestone); }

        set { SetAttribute(TicketAttributes.Milestone, value); }

    }

 

    public string Component

    {

        get { return GetAttribute(TicketAttributes.Component); }

        set { SetAttribute(TicketAttributes.Component, value); }

    }

 

    public string Summary

    {

        get { return GetAttribute(TicketAttributes.Summary); }

        set { SetAttribute(TicketAttributes.Summary, value); }

    }

 

    public string Description

    {

        get { return GetAttribute(TicketAttributes.Description); }

        set { SetAttribute(TicketAttributes.Description, value); }

    }

 

    public string Priority

    {

        get { return GetAttribute(TicketAttributes.Priority); }

        set { SetAttribute(TicketAttributes.Priority, value); }

    }

 

    #region Support methods

 

    private string GetAttribute(string name)

    {

        if (Attributes.Contains(name))

        {

            return Convert.ToString(Attributes[name]);

        }

        return null;

    }

 

    private void SetAttribute(string name, string value)

    {

        if (Attributes.Contains(name))

        {

            Attributes[name] = value;

        }

        else

        {

            Attributes.Add(name, value);

        }

    }

 

    #endregion

}


And last of all we have a class for managing tickets.. by the way this isn't complete, I haven't finished writing it because this is just one of my back burner projects...

public class TicketManager

{

    private ITicket _ticket;

 

    public void Connect(string url, string userName, string password)

    {

        _ticket = XmlRpcProxyGen.Create<ITicket>();

        _ticket.Url = url;

        _ticket.PreAuthenticate = true;

        _ticket.Credentials = new NetworkCredential(userName, password);

    }

 

    public string[] GetAvailableActions(int id)

    {

        return _ticket.GetAvailableActions(id);

    }

 

    public string[] GetAvailableActions(TicketInfo ticket)

    {

        ValidateTicket(ticket);

        return _ticket.GetAvailableActions(ticket.TicketId);

    }

 

    public int[] GetRecentChanges(DateTime since)

    {

        return _ticket.GetRecentChanges(since);

    }

 

    public void DeleteTicket(int ticketId)

    {

        _ticket.Delete(ticketId);

    }

 

    public void DeleteTicket(TicketInfo ticket)

    {

        ValidateTicket(ticket);

        DeleteTicket(ticket.TicketId);

    }

 

    public void UpdateTicket(TicketInfo ticket, string comment)

    {

        ValidateTicket(ticket);

        object[] values = _ticket.Update(ticket.TicketId, comment, ticket.Attributes);

        ticket.Update(values);

    }

 

    public void CreateTicket(TicketInfo ticket)

    {

        if (string.IsNullOrEmpty(ticket.Summary)) throw new ArgumentNullException("ticket.Summary");

        if (string.IsNullOrEmpty(ticket.Description)) throw new ArgumentNullException("ticket.Description");

        if (string.IsNullOrEmpty(ticket.Type)) throw new ArgumentNullException("ticket.Type");

        if (string.IsNullOrEmpty(ticket.Priority)) throw new ArgumentNullException("ticket.Priority");

        if (string.IsNullOrEmpty(ticket.Component)) throw new ArgumentNullException("ticket.Component");

 

        XmlRpcStruct tempAttributes = new XmlRpcStruct();

        foreach (object key in ticket.Attributes.Keys)

        {

            if ((((string)key) != TicketAttributes.Description) && (((string)key) != TicketAttributes.Summary))

            {

                tempAttributes.Add(key, ticket.Attributes[key]);

            }

        }

 

        int id = _ticket.Create(ticket.Summary, ticket.Description, ticket.Attributes);

        ticket.TicketId = id;

    }

 

    private void ValidateTicket(TicketInfo ticket)

    {

        if (ticket == null) throw new ArgumentNullException("ticket");

        if (ticket.TicketId <= 0) throw new ArgumentException("ticketId must be greater then 0");

    }

}


About the only trick here is that some of the date values get returned as unix time stamps so you need to convert them...

public static DateTime ParseUnixTimestamp(double timestamp)

{

    return new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(timestamp);

}


Because the Syzmk Rich Media Processor product I work on is a clearing house for simple messages among other things... It becomes a small jump from here (about 30 lines of C# code, 5 for a quick and dirty "IronPython" script) to start submitting tasks and bugs via email or mobile... who knows, it could be handy if you get a call while your out of the office and need to log a bug or task.
 |  | 
posted @ Monday, October 16, 2006 8:37:48 AM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |

Monorail Pagination with Base4.Net

So, part of today was spent building "digg" style pagination for data I'm pulling out of base4... if you don't use digg, then what I mean is stuff like this:



Flickr and plenty of other sites use the same layout... and it does seem quite a convenient way to work your way through large result sets...

Fairly standard stuff, because I'm using monorail my first pit stop was the pagination helper - however there's a bit of an impedance mismatch here, as the pagination helper is geared towards paging through an IList containing all the records, where as generally speaking you're working with IItemList<T> and wanting to make use of the inherent paging support in base4.net... hmmm... at any rate, this isn't exactly "brilliant" code - but it might prove useful if you messing around with Monorail at home - something I think you should do.

Paging and base4.net

So, first off, I built a small class to represent a "page" from a larger set of query results in my application... it looks like this...

/// <summary>

/// Represents a page from a query

/// </summary>

/// <typeparam name="T"></typeparam>

public class PagedItemList<T> : IEnumerable<T>

    where T: class, IItem

{

    private IItemList<T> _items;              

    private int _pageNumber;

    private int _pageSize;

    private int _totalCount;

 

    public PagedItemList(IItemList<T> items, int pageSize, int pageNumber, int totalCount)

    {

        if (items == null) throw new ArgumentNullException("items");

        if (pageNumber <= 0) throw new ArgumentOutOfRangeException("pageNumber", "pageNumber must be greater then 0");

        if (pageSize <= 0) throw new ArgumentOutOfRangeException("pageSize", "pageSize must be greater then 0");

        if (totalCount < 0) throw new ArgumentOutOfRangeException("totalCount", "totalCount must be greater then or equal to 0");

 

        _items = items;

        _pageNumber = pageNumber;

        _pageSize = pageSize;

        _totalCount = totalCount;                               

    }

 

    /// <summary>

    /// The count of items on this page

    ///