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

    /// </summary>

    public int PageCount

    {

        get { return (int)Math.Ceiling(((double)TotalCount) / ((double)PageSize)); }

    }

 

    /// <summary>

    /// Index of the first item on this page

    /// </summary>

    public int FirstItemIndex

    {

        get

        {

            return PageSize * (PageNumber-1);

        }

    }

 

    /// <summary>

    /// Index of the last item on this page

    /// </summary>

    public int LastItemIndex

    {

        get

        {

            return Math.Min(FirstItemIndex + (PageSize - 1), TotalCount - 1);

        }

    }

 

    /// <summary>

    /// 1-relative page number index

    /// </summary>

    public int PageNumber

    {

        get

        {

            return _pageNumber;

        }

    }

 

    /// <summary>

    /// The size of each page

    /// </summary>

    public int PageSize

    {

        get { return _pageSize; }

    }

 

    /// <summary>

    /// Total number of results returned from the query

    /// </summary>

    public int TotalCount

    {

        get { return _totalCount; }

    }

 

    /// <summary>

    /// Number of items on this page

    /// </summary>

    public int Count

    {

        get { return _items.Count; }

    }

 

    /// <summary>

    /// the underlying list of items

    /// </summary>

    public IItemList<T> Items

    {

        get { return _items; }

    }

 

    public IEnumerator<T> GetEnumerator()

    {

        return _items.GetEnumerator();

    }

 

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

    {

        return ((IEnumerable)_items).GetEnumerator();

    }

}


Pretty mundane, next we just extend my existing repository with a new method for returning a PagedItemList<T>... here's how it looks:

public virtual PagedItemList<T> Find(ObjectQuery query, int pageSize, int pageNumber)

{           

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

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

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

 

    int count = Convert.ToInt32(_context.ExecuteScalar(query.Compile().SELECT_COUNT()));

 

    query.Path.PageSize = pageSize;

    query.Path.PageNumber = pageNumber;

 

    return new PagedItemList<T>(Find(query), pageSize, pageNumber, count);

}


For our query we first estabilsh the total count of results, then we just run the query again and return the results for the selected page.

From Base4.Net to Monorail

Now, our PagedItemList<T> is useful by itself, but there's a more feature-complete interface in monorail for representing a "page" of informaton for display logic, used by the pagination helper, it's called IPaginatedPage.

public interface IPaginatedPage : IEnumerable

{

    int CurrentIndex { get; }   

    int LastIndex { get; }   

    int NextIndex { get; }   

    int PreviousIndex { get; }   

    int FirstIndex { get; }

    int FirstItem { get; }

    int LastItem { get; }

    int TotalItems { get; }

    bool HasPrevious { get; }   

    bool HasNext { get; }

    bool HasFirst { get; }

    bool HasLast { get; }

}


...so, we just build a little adaptor object, using the abstract base page that implementes IPaginatedPage, called "AbstractPage" funnily enough - here I've called my adaptor Base4Page.

public class Base4Page<T> : AbstractPage

    where T: class, IItem

{

    private PagedItemList<T> _items;

 

    public Base4Page(PagedItemList<T> items)

    {

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

        _items = items;

 

        CalculatePaginationInfo(items.FirstItemIndex, items.LastItemIndex, items.TotalCount, items.PageSize, items.PageNumber);

    }

 

    public override System.Collections.IEnumerator GetEnumerator()

    {

        return _items.GetEnumerator();

    }

}


Soo... last of all, where's the pay off?

Well, first thing's first, now you can do this in your controller:

public void Users(int pageSize, int page)

{           

    ObjectQuery query = new ObjectQuery(typeof(Car));
    // not enough set some further critiera...

    PropertyBag.Add("Cars", new Base4Page<Car>(_carRepository.Find(query, pageSize, page)));

}


At which point, you could do some simple pagination in your views (and pagination examples for existing Monorail websites can be cut 'n' pasted in to give you a head start)...

<table cellpadding="4">
<tr>
<th>Make</th>
<th>Model</th>
<th>Year</th>
</tr>
#foreach($Car in $Cars)
<tr>
<td>$!Car.Make</td>
<td>$!Car.Model</td>
<td>$!Car.Year</td>
</tr>
#end
</table>
<div class="pagination" id="pagination">
<table width="90%" border="0">
<tr>
<td>Showing $Cars.FirstItem - $Cars.LastItem of $Cars.TotalItems</td>
<td align="right">
#if($Cars.HasFirst) $PaginationHelper.CreatePageLink( 1, "first", null, null) ) #end
#if(!$Cars.HasFirst) first #end
#if($Cars.HasPrevious) | $PaginationHelper.CreatePageLink( $Cars.PreviousIndex, "prev", null, null) ) #end
#if(!$Cars.HasPrevious) | prev #end
#if($Cars.HasNext) | $PaginationHelper.CreatePageLink( $Cars.NextIndex, "next",null, null) ) #end
#if(!$Cars.HasNext) | next #end
#if($Cars.HasLast) | $PaginationHelper.CreatePageLink( $Cars.LastIndex, "last",null, null) ) #end
#if(!$Cars.HasLast) | last #end
</td>
</tr>
</table>
</div>

And of course it's not much harder to write the logic for displaying "digg" style pagination.... I decided to write it as a helper in C#, because after messing with it for 15 minutes in brail it pissed me off too much (some things in brail are quite annoying compared to normal boo, for instance I couldn't seem to use the "range(...)" builtin, and if your view doesn't have any content after the last <% ... %> block it throws up a compile time exception... Admittedly I haven't done a get latest from the Castle site in a couple of weeks... so this might not actually be a problem any more, or maybe I just don't have the brail engine configure "just right".

public abstract class AbstractDiggPaginationHelper : AbstractHelper

{

    public string CreateDiggPagination(IPaginatedPage page, int adjacents)

    {

        return CreateDiggPagination(page, adjacents, null);

    }

 

    public string CreateDiggPagination(IPaginatedPage page, int adjacents, IDictionary queryStringParams)

    {

        StringBuilder output = new StringBuilder();

        WriteLink(output, page.PreviousIndex, "« prev", !page.HasPrevious, queryStringParams);

 

        if (page.LastIndex < (4 + (adjacents * 2))) // not enough links to make it worth breaking up

        {

            WriteNumberedLinks(output, page, 1, page.LastIndex, queryStringParams);

        }

        else

        {               

            if ((page.LastIndex - (adjacents * 2) > page.CurrentIndex) && // in the middle

                    (page.CurrentIndex > (adjacents * 2)))

            {

                WriteNumberedLinks(output, page, 1, 2, queryStringParams);

                WriteElipsis(output);

                WriteNumberedLinks(output, page, page.CurrentIndex - adjacents, page.CurrentIndex + adjacents, queryStringParams);

                WriteElipsis(output);

                WriteNumberedLinks(output, page, page.LastIndex - 1, page.LastIndex, queryStringParams);

            }

            else if (page.CurrentIndex < (page.LastIndex /2))

            {

                WriteNumberedLinks(output, page, 1, 2 + (adjacents * 2), queryStringParams);

                WriteElipsis(output);

                WriteNumberedLinks(output, page, page.LastIndex - 1, page.LastIndex, queryStringParams);

            }               

            else // at the end

            {

                WriteNumberedLinks(output, page, 1, 2, queryStringParams);                   

                WriteElipsis(output);

                WriteNumberedLinks(output, page, page.LastIndex - (2 + (adjacents * 2)), page.LastIndex, queryStringParams);

            }

        }

 

        WriteLink(output, page.NextIndex, "next »", !page.HasNext, queryStringParams);

        return output.ToString();

    }

 

    private void WriteElipsis(StringBuilder builder)

    {

        builder.Append("...");           

    }

 

    private void WriteNumberedLinks(StringBuilder builder, IPaginatedPage page, int startIndex, int endIndex, IDictionary queryStringParams)

    {           

        for (int i=startIndex; i<= endIndex; i++)

        {

            WriteNumberedLink(builder, page, i, queryStringParams);

        }

    }

 

    private void WriteLink(StringBuilder builder, int index, string text, bool disabled, IDictionary queryStringParams)

    {

        if (disabled)

        {

            builder.AppendFormat("<span class=\"disabled\">{0}</span>", text);

        }

        else

        {

            WritePageLink(builder, index, text, null, queryStringParams);

        }

    }

 

    private void WriteNumberedLink(StringBuilder builder, IPaginatedPage page, int index, IDictionary queryStringParams)

    {

        if (index == page.CurrentIndex)

        {

            builder.AppendFormat("<span class=\"current\">{0}</span>", index);

        }

        else

        {

            WritePageLink(builder, index, index.ToString(), null, queryStringParams);

        }

    }

 

    protected abstract void WritePageLink(StringBuilder builder, int page, String text, IDictionary htmlAttributes, IDictionary queryStringParams);

 

}


The guts of the helper is in an abstract class, simply so I could test the numbering logic without having to worry about creating a controller & http context (just implement the WritePageLink method with some simple text output ... the implementation is here:

public class DiggPaginationHelper : AbstractDiggPaginationHelper

{

 

    protected override void WritePageLink(StringBuilder builder, int page, String text, IDictionary htmlAttributes, IDictionary queryStringParams)

    {

        string filePath = "";

 

        if (CurrentContext != null)

        {

            filePath = CurrentContext.Request.FilePath;

        }

 

        if (queryStringParams == null)

        {

            queryStringParams = new Hashtable();

        }

 

        queryStringParams["page"] = page.ToString();

 

        builder.AppendFormat("<a href=\"{0}?{1}\" {2}>{3}</a>", filePath, BuildQueryString(queryStringParams), GetAttributes(htmlAttributes), text);

    }

}


At which point you can just register it on the controller, and call it with something like ${DiggPaginationHelper.CreateDiggPagination(tracks, 3)} and get yourself a nice little paging display.  The second parameter is the "adjacent" - which can be used to control how many pages are displayed either side of the selected page when dealing with lots of results.

Oh, and of course you might want some CSS to go with it...

/* pagination */

 

div.pagination {

    padding: 3px;

    margin: 3px;

}

 

div.pagination a {

    color: #000099;

    text-decoration: none;   

    padding: 2px 5px 2px 5px;

    margin: 2px;

    border: 1px solid #AAAFEE;   

}

 

div.pagination a:hover, div.pagination a:active {

    color: #000;

    border: 1px solid #000099;   

}

 

div.pagination span.current {   

    font-weight: bold;

    background-color: #000099;

    color: #FFF;

    padding: 2px 5px 2px 5px;

    margin: 2px;

    border: 1px solid #000099;

}

 

div.pagination span.disabled {

    color: #DDD;

    padding: 2px 5px 2px 5px;

    margin: 2px;

    border: 1px solid #EEE;   

}



 |  | 
posted @ Monday, October 16, 2006 4:40:14 AM (New Zealand Daylight Time, UTC+13:00)    Comments [2] | Trackback |
 Wednesday, October 11, 2006

Base4 & Castle

Well I've been working with HTML & style sheets today (mostly) - It's be a long while since I've done web development, so it was a little tedious, and it did briefly cross my mind that life might be easier if we lived in some kind of dictatorship where style sheets and layout were provided for me by the "man"... at any rate I also managed to fit in a bit of monorail and base4 to keep my brain from freezing over.

I think for this post I might just talk about the way I'm using base4... basically I prototyped some stuff and came to the conclusion that I didn't like the smell of the static StorageContext class in base4 and it's default connection - too hard to test against - not to say that it's a bad idea, just that I couldn't see any easy way to test and mock code  for code that used/consumed it... so I decided to work up a simple alternative using castle's IoC and some generic interfaces...

At this point... If you've been using Castle for more then a couple of week this is all old news I'm sure, so probably better off finding something else to read ;o)

The facility

First off we have a facility:

public class Base4StorageFacility : AbstractFacility

{

    protected override void Init()

    {

        string url = FacilityConfig.Attributes["url"];

        if (string.IsNullOrEmpty(url))

        {

            throw new StorageException("The Base4StorageFacility requires a \"url\" attribute to be set");

        }

 

        Kernel.AddComponentInstance("base4.defaultContext", typeof (IItemContext), StorageContext.NewContext(url));

        Kernel.AddComponent("base4.repository", typeof (IRepository<>), typeof (Base4Repository<>));

    }

}


A facility extends the container with additional functionality, in this case the only reason we're using a facility (instead of registering the components themselves individually) is because we're using a static method to create our context for a specific base4 connection url.

Moving on from here we can register the facility in the containers configuration, incidentally now I have somewhere pleasant to configure what base4 server I connect to by default in my application.

<facility id="base4.storageFacility"

    type="MyProject.Core.Base4StorageFacility, MyProject.Core"

    url="tcp://Server:@localhost:999/Base4_default" />


The Repository

Now what about the IRepository<> ? well here's the interface:

public interface IRepository<T>

    where T : class, IItem, new()

{

    IItemListProxy<T> List();

    IItemList<T> FindAll();

    IItemList<T> Find(ObjectQuery query);

    IItemList<T> Find(string path);

    IItemList<T> Find(string path, params string[] replaces);

    IItemList<T> Find(ObjectPath path);

    IItemList<T> Find(ObjectPath path, ObjectScope scope);

    IItemList<T> FindUsingSQL(string SQL);

    IItemList<T> FindUsingSQL(string SQL, ObjectScope scope);

    T Get(T previous);

    T Get(object id);

    T Get(ItemKey<T> key);

    T Get(string relativeUri);

    T FindOne(ObjectQuery query);

    T FindOne(string opath);

    T FindOne(string path, params string[] replaces);

    T FindOne(ObjectPath path);

    T FindOneUsingSQL(string SQL);

    T FindOneUsingSQL(string SQL, ObjectScope scope);

    T FindOne(string opath, ObjectScope scope);

    T FindOne(ObjectPath path, ObjectScope scope);

    void DeleteAll();

    void Delete(ObjectPath path);

    void Delete(string path);

    void Delete(string path, params string[] replaces);

    void Delete(T item);

    void Save(T item);

    T Create(string Xml);

    T Create(XmlReader reader);

    T Create();

}


It's pretty much works like IItemContext, except that you avoid having to pass generic parameters to the individual methods because the interface itself has a generic parameter... there's a couple of extras there that I'll cover at the end of this post too.

Conversely, the class Base4Repository<T> implements this interface... which looks like this (or at least the first few methods, I've left the rest out for brevity) - this is similar to what Ivan has done.

public class Base4Repository<T> : IRepository<T>

    where T : class, IItem, new()

{

    private IItemContext _context;

 

    public Base4Repository(IItemContext context)

    {

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

        _context = context;

    }

 

    public virtual IItemListProxy<T> List()

    {

        return _context.List<T>();

    }

 

    public virtual IItemList<T> FindAll()

    {

        return _context.FindAll<T>();

    }


Notice that it doesn't have a parameterless constructor, we rely on the container to inject the default IItemContext when creating instances of the Base4Repository...

The Container

Now, by default the container assumes a component has a "singleton" lifestyle, thankfully for a type with a generic parameter it is per that parameter, so this test case below passes - incidentally if you tend to use lifecycles other then the default, I would strongly suggest adding tests to make sure the lifecycle is actually applied... you can just imagine what happens in a multi-threaded app when a "Message" class has a singleton lifecycle when you expected a transient ;o) you end up with some bizarre behavior that might not be picked up in normal unit tests.

[Test]

public void IsSingleton()

{

    IRepository<FileBase> fileRepository1 = container.Resolve<IRepository<FileBase>>();

    IRepository<FileBase> fileRepository2 = container.Resolve<IRepository<FileBase>>();

    Assert.AreSame(fileRepository1, fileRepository2);

 

    IRepository<TypeImpl> typeRepository1 = container.Resolve<IRepository<TypeImpl>>();

    IRepository<TypeImpl> typeRepository2 = container.Resolve<IRepository<TypeImpl>>();

    Assert.AreSame(typeRepository1, typeRepository2);

}


cool, clear as mud?

Right... so moving on from there, in my own components when I need to access data I now use the repository... so I could, for instance, create a monorail controller like this:

public class FileController : BaseController

{

    private IRepository<FileBase> _repository;

 

    public FileController(IRepository<FileBase> repository)

    {

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

        _repository = repository;

    }

 

    public void Fetch(Guid fileId)

    {

        CancelLayout();

        CancelView(); // very important

        Response.Clear(); // ensure the response is empty

 

        FileBase file = _repository.Get(fileId);

        if (file != null)

        {               

            Response.ContentType = file.MimeType;

            file.FileContent.CopyTo(Response.OutputStream);

        }

        else

        {               

            Response.StatusCode = 404;

        }

    }       

}


And everything will just work - now in some cases this isn't convenient (the dependency injection model) so I broke down and created a static repository class as well, which accesses the default container for the application... basically it has the same interface as IRepository, but they're static methods.. so you can code like this:

Track track = Repository<Track>.Get(id);


Personally I'm not actually that keen on this approach - The Syzmk RMP product I've worked on uses the container everywhere, but avoids ever having to access the default container statically...  and if you end up with a class being injected with a large number of dependencies it's often (but not always) a good indication that there's some violation of orthogonality - if only because a class consuming that many dependencies is probably doing more then one thing... a little difficult to pick up on otherwise.

But at any rate It seems pretty good for a website, where I can't see me using more the one container (or even child containers) within the same app domain.

Wrinkles...

Moving beyond that, the last thing I have to say is that originally I was creating new instances and then saving them with a repository like this:

Group group = new Group();

...
Repository<Group>.Save(group);

... 

User user = new User();

...
user.Groups.Add(group);

...
Repository<User>.Save(user);


However, it all turns to 4 shades of brown when we go to add to a many-to-many collection, like this:

Goat goat = new Goat();

goat.Name = "junk";

Repository<Goat>.Save(goat);

 

BoatOfGoats boat = new BoatOfGoats();

boat.Name = "track";

boat.Goats.Add(goat); // <-- "StorageException : Default is not available, as no default has been set"

Repository<BoatOfGoats>.Save(track);


As far as I can tell the event handling for the Add(item) method expects a default context to be assigned... now I'd be tempted to call this a bug, but that's a bit rash till I understand the in's and out's of base4, however there's an easy way round it... and that's to assign the context yourself :) so a fix would be to add this line before the one that bombs:

track.Context = container.Resolve<IItemContext>();


But that's pretty kludgey, so instead of using:

Goat goat = new Goat();


I added some Create(...) overloads to the repository to take care of it... and do this:

Genre genre = Repository<Genre>.Create();

Last of all, though I haven't drawn on the entire implementation, I'm hoping to follow (At least in spirit) some of the work Ayende has done with his NHibernate repository concept as the project progresses, I think it will add a more natural "feel" combined with the repositories for implementing transactions vs. interacting with base4's ObjectTransaction directly, not to mention providing something I can test and mock out easily...  We shall see what actually happens as the project progresses.



 |  | 
posted @ Wednesday, October 11, 2006 7:34:54 AM (New Zealand Daylight Time, UTC+13:00)    Comments [3] | Trackback |
 Tuesday, October 10, 2006

Just hailed like hell here, thought a window was going to break ;o)







posted @ Monday, October 09, 2006 11:58:40 PM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |