Programming, technology, and CRM – from a Belgian programmer exiled to Missouri
  • rss
  • Home
  • Contact Me
  • Welcome

Hosting WCF Services within SalesLogix

Nicolas Galler | June 24, 2008

The recent releases of WCF brought a whole bunch of goodies: most notable for me are the added support for syndication and script services, as well as a "no-configuration" scenario where we did not need to add a whole page of XML in web.config anymore just to get a basic service running.

I knew that there would be a few problems to getting it to collaborate with the Saleslogix web client app because of its tight integration with the ASP.NET pipeline, but wanted to take a look anyway.

I started with this very simple service file:

<%@ ServiceHost Language="C#" Debug="true"
    Service="SSSWorld.Scratch.SimpleService"
    CodeBehind="SimpleService.svc.cs"
    Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory,
      System.ServiceModel.Web,
      Version=3.5.0.0,
      PublicKeyToken=31bf3856ad364e35" %>

The key points is the "WebScriptServiceHostFactory.  Basically this automatically configures the endpoint for WCF without the need to add it to the web.config.

My SimpleService.cs at this point is also very simple:

public class SimpleService : ISimpleService
{
    public String GetData()
    {
        return "hello";
    }
}

And this is the corresponding "ServiceContract" (aka interface):

[ServiceContract]
public interface ISimpleService
{
    [OperationContract]
    [WebGet]
    String GetData();
}

The first problem I ran into appears to have actually been an installation problem.  I kept getting an error "Unable to load System.ServiceModel.Web".  Eventually I re-registered the assembly with the GAC (with gacutil /i /F) and all was good on that side.

Next was an IIS configuration problem.  Apparently integrated Windows authentication needs to be disabled.  So I cut it off (if you still wanted it to work with Saleslogix you could just cut it off for the folder containing the .svc file, I suppose) and finally got a

{"d":"hello"}

back at me.

Next I wanted to be able to access the Saleslogix service, so I added the following code:

if (ApplicationContext.Current == null)
                throw new InvalidOperationException("No Application Context");

Got the expected exception thrown.  I knew that ApplicationContext relies on classic ASP.NET sessions and also has some hard-coded dependencies to HttpContext.Current so I needed to enable the ASP.NET compatibility of WCF.  This is done with 2 changes, first I needed to add an attribute on the SimpleService class:

[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]

And I also needed the following blurb in web.config (which I was a little bit sad about since I was hoping not to have to modify it, but oh well):

<system.serviceModel>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel> 

At this point I got my "hello" back.

Now it was time to see how far we could go!  I changed "GetData" into a simple search service – what it would do is simply return a little bit of information about accounts matching the input string (well, just the account name, for now):

public String GetData(String search)
{
    if (ApplicationContext.Current == null)
        throw new InvalidOperationException("No Application Context");
    if (String.IsNullOrEmpty(search))
        return "";
    using (SessionScopeWrapper session = new SessionScopeWrapper(true))
    {
        var matches = session.CreateQuery("from Sage.SalesLogix.Entities.Account a where a.AccountName like ?")
            .SetString(0, search + "%")
            .List<IAccount>();
        if (matches.Count > 0)
        {
            return matches[0].AccountName;
        }
        else
        {
            return "";
        }
    }
}

Go to http://…../SimpleService.svc/GetData?search=Abbott (going through the login page if necessary) and this returns:

{"d":"Abbott Ltd."}

as expected.

Getting pretty close!  Now if you wanted to return a list with some more info about each account, how about this:

  • Create a data contract:
[DataContract]
public class AccountInfo
{
    [DataMember]
    public String AccountName { get; set; }

    [DataMember]
    public String MainPhone { get; set; }

    [DataMember]
    public String City { get; set; }
}
  • Change the declaration:
[OperationContract]
[WebGet]
IList<AccountInfo> GetData(String search);
  • Change the "return matches[0].AccountName" line into something like this:
var q = from m in matches
        select new AccountInfo
        {
            AccountName = m.AccountName,
            MainPhone = m.MainPhone,
            City = m.Address.City
        };
return q.ToList();

Well, you get the idea.  The data can be recovered from a script service on an ASP.NET page, etc.

Now there is one serious limitation – this requires the user to be properly authenticated (because otherwise SalesLogix can’t form the connection string!).  If we need the service to be accessible from an outside client (eg for a mashup or an RSS feed) you would have to either somehow simulate a user login, or forego the whole ApplicationContext, host your service outside of the web client, and maintain the connection string for it separately.  At this point you could either use the technique I outlined in my "Unit Testing" posts to still get access to the NHibernate session (and to the entity business rules!), or just use regular ADO.NET to retrieve the data (probably easier, more reliable, and yielding better performance, unless you need access to the business rules.

Anyway, this was an interesting exploration, I have not decided if I would make anything of it yet.

Comments
1 Comment »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Sending an email from Saleslogix (or ASP.NET)

Nicolas Galler | June 23, 2008

As the .NET framework is available to code executing on the server side of the Saleslogix web client it is easy to send an email using System.Net.Mail etc.  Lots of available examples for that so I won’t repeat them.

The problem with this approach (for some scenario, anyway) is that the user does not have a chance to review and edit the text the email (or attach it to Saleslogix for that matter).  Sometimes it would be nicer to just open the email in Outlook and let them complete it, and in some cases it is possible with a little bit of JavaScript, as long as you are able to get your users to adjust their IE security settings to enable unsafe ActiveX (which they have to do for the export to Excel anyway).

My problem was a simple "Quote" screen where the user should be able to print the quote report, have it attached to history and have the opportunity to send an email to the quote contact:

image

This is simple enough to do in the regular Saleslogix client – in the web client there are 2 problems: one is creating and displaying the message in Outlook, the other one is attaching the report (which was exported as a PDF on the server but will not be available from the client side).

In order to resolve the second problem, and since we can give Outlook a URL to attach, I created a "GetReport" service that is placed in an unsecured directory (since it will have to be accessed from Outlook) and simply returns the PDF data.  As a side note, to unsecure a page, place a web.config file in its directory with the following:

<?xml version="1.0"?>

<configuration>
  <location path="GetReport.ashx">
    <system.web>
      <authorization>
        <allow users="*"/>
      </authorization>
    </system.web>
  </location>
</configuration>

Now you can get to it without logging in.  Which is OK for this service since you need to have a report token in order to retrieve anything.

To resolve the first problem I sent some code (with ScriptManager.RegisterClientBlock) that will open Outlook on the client, fill in the subject and recipient, and display the message so the user can finish editing it:

public void ShowReportEmail(Uri reportUrl, string to, string cc, string subject, string body)
{
    StringBuilder script = new StringBuilder();

    var msgData = new { to = to, cc = cc, subject = subject, body = body, reportUrl = reportUrl.ToString() };
    var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    script.Append("try {\n" +
        "var outlook = new ActiveXObject(\"Outlook.Application\");\n" +
        "var msg = outlook.CreateItem(0);\n")
        .AppendFormat("var msgData = {0};\n", serializer.Serialize(msgData))
        .Append("msg.Subject = msgData.subject;\n" +
            "msg.Body = msgData.body;\n" +
            "msg.To = msgData.to;\n" +
            "msg.Cc = msgData.cc;\n" +
            "msg.Attachments.Add(msgData.reportUrl);\n" +
            "msg.Display();\n" +
            "} catch(e) {\n" +
            "  alert('Error accessing outlook: ' + e.description + '.\\nPlease consult your administrator on recommended IE settings.');\n" +
            "}");

    ScriptManager.RegisterClientScriptBlock(this, GetType(), "EmailReport", script.ToString(), true);
}

The code makes use of the anonymous object syntax so requires VS 2008 to compile (should not require .NET 3.5 to run, though).  You could adapt easily if needed.

This probably works on Outlook 2003 and higher, but I only tested on Outlook 2007.

The end result looks like this… I am not thrilled about the fact that the attachment name looks so crappy but other than that I quite like it:

image

And this is by the way the setting that needs to be enabled in IE… you probably only want to enable that for trusted sites:

image

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Random Pitfalls of Saleslogix Web Client

Nicolas Galler | June 20, 2008

Just a couple observations, figured I might as well put them here so I don’t forget.

1. OnCreate May Be Called more than once (Update – I don’t think this is true anymore on 7.5.1)

Specifically, if you are writing an insert page, the OnCreate event of the entity will be called on EVERY POSTBACK.  This surprising behavior just cost me two hours.

2. DataBinding Only Works if an Event is Defined

For example, if you bind to the SelectedValue of a control, but the control does not have a SelectedValueChanged event, the property will never get updated on the entity.

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Strongly Typed Databindings in SlxWeb

Nicolas Galler | June 13, 2008

I wanted to share a useful little trick to avoid typos when doing the bindings on a custom smart part.

The default (in the code generated by the AA) looks like this:

Sage.Platform.WebPortal.Binding.WebEntityBinding nmeContactNameNamePrefixBinding = new   Sage.Platform.WebPortal.Binding.WebEntityBinding("Prefix", nmeContactName, "NamePrefix");
BindingSource.Bindings.Add(nmeContactNameNamePrefixBinding);

You can also write it a bit more succinctly as this, but it still requires you to remember how to spell the entity property as well as the control property:

BindingSource.Bindings.Add(new WebEntityBinding("Prefix", nmeContactName, "NamePrefix"));

I like it like this, where the Intellisense can do the autocomplete for me:

this.BindingSource.Bindings.Add(t.CreateBinding(s => s.Cust

To this end I have this tiny helper class:

public class TypedWebEntityBindingGenerator<TEntity>
{
    public WebEntityBinding CreateBinding<TComponent, TProperty, TProperty2>(Expression<Func<TEntity, TProperty>> entityProperty,
        TComponent component,
        Expression<Func<TComponent, TProperty2>> componentProperty)
    {
        var propFrom = BuildPropertyAccessString((MemberExpression)entityProperty.Body);
        var propTo = (PropertyInfo)((MemberExpression)componentProperty.Body).Member;
        return new WebEntityBinding(propFrom, component, propTo.Name);
    }

    private String BuildPropertyAccessString(MemberExpression memberExpression)
    {
        String b = "";

        if (memberExpression.Expression is MemberExpression)
            b = BuildPropertyAccessString((MemberExpression)memberExpression.Expression) + ".";
        return b + ((PropertyInfo)memberExpression.Member).Name;
    }
}

You will have to set up your project to target the .NET 3.5 framework.

This *may* also requires you to enable .NET 3.5 in the web.config.  YMMV.  To enable .NET 3.5 you basically replace all occurrences of 1.0.61025.0 with 3.5.0.0 and add the following snippet under the assemblyBinding element in web.config:

<dependentAssembly>
    <assemblyIdentity name="System.Web.Extensions" culture="neutral" publicKeyToken="31bf3856ad364e35"/>
    <bindingRedirect oldVersion="1.0.61025.0"
                     newVersion="3.5.0.0"/>
</dependentAssembly>
Comments
2 Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Creating and Displaying a Group Programmatically

Nicolas Galler | June 12, 2008

1. The Players

GroupContext.GetGroupContext() is used to retrieve the class holding the info about what group the user currently has selected for each entity.  It is stored in the session.  GroupContext.GetGroupContext() is currently hard-coded to reference the HTTP context so do not call it outside of the web client.

GroupContext has an "EntityGroupInfo" object for each entity.  This EntityGroupInfo is a sort of cache for the currently selected group as well as a frontend for group selection.  It does not have a direct relationship to the GroupInfo class which actually represents a specific group.

GroupTranslator is the COM object that is responsible for translating XML to Delphi blob and vice versa.

2. Creating a Group

Saving a new group is relatively easy.  There is a GroupInfo.Save method which saves a group and returns the group id.  It does not work outside of the web client though so I just used the GroupTranslator directly.

try
{
    String groupXml = GroupInfo.GetBlankGroupXML("Contact");
    // this retrieves the condition as a subquery
    // (QueryBuilder is another piece I have that builds the query - anything that will
    // return a query string will work, though)
    String condition = ((QueryBuilder)group.CreateKeyFieldQuery(false)).GetSqlQuery(true);
    GroupInfo ginfo = new GroupInfo();
    ginfo.GroupXML = groupXml;
    ginfo.GroupName = groupName;
    ginfo.AddLookupCondition("CONTACT:CONTACTID", " IN ", "(" + condition + ")");

    // remove the existing group
    _groupManager.DeleteGroup("Contact", groupName, "ADMIN");

    // we could just use ginfo.Save here but it doesnt work without an active web session
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(ginfo.GroupXML);
    XmlElement pluginDataNode = (XmlElement)doc.SelectSingleNode("SLXGroup/plugindata");
    pluginDataNode.Attributes["name"].Value = groupName;
    pluginDataNode.Attributes["displayname"].Value = groupName;
    pluginDataNode.Attributes["id"].Value = "";
    doc.SelectSingleNode("SLXGroup/groupid").InnerText = "";
    String groupId = translator.SaveGroup(doc.OuterXml, GroupInfo.ConnectionString);
}
finally
{
    System.Runtime.InteropServices.Marshal.ReleaseComObject(translator);
}

3. Displaying the Group

If you try to display the group by redirecting the browser to Contact.aspx?gid=…., there is a good chance they will get a NullReferenceException at that point.  This is because the GroupContext has a cache of the active groups for the entity and this cache is not automatically rebuilt.

The following code will take care of clearing this.  Note that I have to catch the NullReferenceException because of the way GroupContext works outside of the web client and I have this class unit-tested.  The call to ClearCache and the setting of the CurrentTable are the other tricky parts necessary.

try
{
    if (GroupContext.GetGroupContext() != null)
    {
        LOG.Debug("Setting group context");
        GroupContext.GetGroupContext().CurrentTable = "CONTACT";
        var groupCache = GroupContext.GetGroupContext().GetGroupInfoForTable("CONTACT");
        groupCache.ClearCache();
        groupCache.CurrentID = groupId;
    }
    else
    {
        LOG.Debug("Group Context is not available.");
    }
}
catch (NullReferenceException)
{
    // ignore those because GroupContext is f. up
}

After that you can redirect to "Contact.aspx?gid=" + groupId and have the new group displayed.  Or maybe just redirect to Contact.aspx since we set the current group id (didn’t test that one).

4. Displaying a Temporary Group

There is a SetCurrentGroupAsLookupResult method in the EntityGroupInfo object, however I could not get it to work.

I am out of time for today but would love to know if anyone figures this one out.

Comments
No Comments »
Categories
Saleslogix
Comments rss Comments rss
Trackback Trackback

Web Services in Saleslogix

Nicolas Galler | June 11, 2008

Just a quick tip in case you need to define a web service that accesses the Saleslogix entities – it works great, as long as you remember to enable the attribute to let it access the session variables (session state is disabled by default on web services).

For example this is the web service I use for a cascading dropdown (using the Ajax control toolkit stuff).  The EnableSession=true was the key part in getting this to work:

[System.Web.Services.WebMethod(EnableSession=true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public CascadingDropDownNameValue[] GetSubSpecialties(string knownCategoryValues, string category)
{
    StringDictionary kv = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
    if (!kv.ContainsKey("specialty"))
        return null;
    var pklItems = PickList.GetPickListItemsByName("SubSpecialty " + kv["specialty"], true);
    var values = new List<CascadingDropDownNameValue>();
    foreach (var p in pklItems)
    {
        values.Add(new CascadingDropDownNameValue(p.Text, p.Code));
    }
    return values.ToArray();
}
Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Saving an Attachment to Saleslogix

Nicolas Galler | June 4, 2008

This is an example of how much simpler, cleaner and more powerful the code in the new client is.  Here is a method that will take an arbitrary entity (well, arbitrary to a point – it has to be one of the "TALCO" entities) and write an attachment under it.

/// <summary>
/// Create a new attachment for the designated file.
/// </summary>
/// <param name="entityType">(Case sensitive) entity type that the attachment is going to be related to (eg Account, Opportunity)</param>
/// <param name="entityId">PK</param>
/// <param name="path">Absolute path to file to be attached.  
///  It will be copied to the attachment path.</param>
public object SaveAttachment(string entityType, object entityId, string path, string description)
{
    Type entityTypeActual = Type.GetType("Sage.Entity.Interfaces.I" + entityType + ",Sage.Entity.Interfaces");
    if (entityTypeActual == null)
        throw new InvalidOperationException("Invalid entity type " + entityType);
    object parentEntity = EntityFactory.GetById(entityTypeActual, entityId);
    if(parentEntity == null)
        throw new InvalidOperationException("Invalid entity id or type: " + entityType + "/" + entityId);
    IAttachment attachment = EntityFactory.Create<IAttachment>();
    switch (entityType)
    {
        case "Account":
            attachment.AccountId = (String)((IAccount)parentEntity).Id;
            break;
        case "Opportunity":
            attachment.AccountId = (String)((IOpportunity)parentEntity).Account.Id;
            attachment.OpportunityId = (String)((IOpportunity)parentEntity).Id;
            break;
        case "Contact":
            attachment.AccountId = (String)((IContact)parentEntity).Account.Id;
            attachment.ContactId = (String)((IContact)parentEntity).Id;
            break;
        case "Ticket":
            attachment.AccountId = (String)((ITicket)parentEntity).Account.Id;
            attachment.ContactId = (String)((ITicket)parentEntity).Contact.Id;
            attachment.TicketId = (String)((ITicket)parentEntity).Id;
            break;
        case "Lead":
            attachment.LeadId = (String)((ILead)parentEntity).Id;
            break;
        default:
            throw new InvalidOperationException("Unsupported entity type " + entityType);
    }

    attachment.AttachDate = DateTime.Now;
    attachment.Description = description ?? System.IO.Path.GetFileName(path);
    // save the attachment so that the Id property is populated
    attachment.Save();
    // copy the path to the attachment folder and save the attachment record
    // this will also populate the user
    attachment.UpdateFileAttachment(path);
    return attachment.Id;
}

And this is a unit test for it:

/// <summary>
///A test for SaveAttachment
///</summary>
[TestMethod()]
public void SaveAttachmentTest1()
{
    AttachmentWriter target = new AttachmentWriter();
    using (ISession sess = new SessionScopeWrapper())
    {
        IAccount acc = sess.CreateQuery("from Sage.SalesLogix.Entities.Account order by newid()").List<IAccount>()[0];
        string entityId = (string)acc.Id;
        string entityType = "Account";
        string path = "dynamicmethods.xml";
        string description = "dynamicmethods";
        object attachId = target.SaveAttachment(entityType, entityId, path, description);
        IAttachment attach = EntityFactory.GetById<IAttachment>(attachId);
        Assert.IsNotNull(attach);
        try
        {
            String attachPath = Path.Combine(Sage.SalesLogix.Attachment.Rules.GetAttachmentPath(),
                attach.FileName);
            Assert.IsTrue(File.Exists(attachPath), "File was not copied: " + attach.FileName);
            File.Delete(attachPath);
        }
        finally
        {
            attach.Delete();
        }
    }
}
Comments
No Comments »
Categories
Programming, Saleslogix
Tags
Attachment, Saleslogix
Comments rss Comments rss
Trackback Trackback

Using Saleslogix Web

Nicolas Galler | June 2, 2008

Today I am using the web client as I am working from home (we track our development projects using tickets in Saleslogix so I actually do use Saleslogix continuously throughout the day… I find this is a very good thing as it gives me a lot of "user-side" insight).  I thought this would be a good occasion to gather thoughts on a lot of annoyances in the web client and reasons that I do not find it an acceptable alternative to the current LAN client, from a user point of view instead of my usual developer point of view.  I am not going to talk about the bugs like the fact that you can randomly get disconnected or the fact that the ticket punch out doesn’t work half the time – just the usability issues. 

These are in no particular order (other than the order in which they presented themselves to me):

  1. Performance.  Even though there have been considerable improvements with the last service pack it still takes too long to log in, switch tab, switch between groups, switch between main views.  Also there are too many postbacks when selecting values in the form and they are too slow (e.g. after selection of area/category/issue you have to wait 2 to 3 seconds before continuing your edits…).
  2. Keyboard focus in lookups.  When I open a lookup I would like to be able to type in the search field, press enter, move the selection using the arrow, and press enter again to select the record.
  3. Keyboard navigation in forms.  You can’t use the application during the whole day if you have to pick each answer with the mouse.  Typically this is something that will be very important to our most ardent Saleslogix users, so not something you want to skimp on.
    1. No clear indication of where the focus is
    2. Shift-tab (to cycle back through the controls) does not work for some reason (don’t know if it is an IE deficiency or something broken with the web client.  But it is annoying)
    3. Doesn’t seem to be a way to select a picklist value using the keyboard.
    4. Same with the date controls
  4. Groups UI.  Well, they are revamping that in 7.5, so I don’t see any need to expand on that.
  5. Groups.  Too many times I have a group that works perfectly in the LAN client but crashes the web client.
  6. Too easy to lose unsaved data.  We need a "You have unsaved data" prompt.  Or an autosave as on the LAN client.  The little reminder in the lower left corner is not enough (not to mention the fact that it often pops up when nothing has been modified, so I really don’t pay attention to it anymore).
  7. Form layout.  The size of the popup rarely fits the contained form and you are often left with either a bunch of blank space on the sides or scrollbars.  It is ugly and unprofessional looking.
  8. Calendar.  The calendar is completely unusable if you have more than 2 or 3 appointments within a month due to the abysmal performance.  Also I am not sure whether that normally syncs with Outlook (I have the ActiveX disabled so couldn’t check).
  9. A lot of the tabs where they did not spend the extra development time are not as usable as they should be: not sortable, default column width are not appropriate, there is a big ugly "Edit" column to bring up the popup.
  10. Unable to drag attachments to the page – this is another big one for me as we use a lot of attachments to track progress

Before this is mistaken for a rant post I would like to balance the above points by the fact that there are a few things that actually work better in the web client!

  1. Picklists look better.  Multi select picklists need some work though.
  2. I like the fact that it keeps coming back to the selected group instead of throwing you to a lookup result group, when you click on a link to edit a single record.
  3. It is nice to have the multiple link columns and be able to go to either the Ticket, Account or Contact from the group view.  Although I wish you could double click on the row to get the default link.
  4. With IE7 or Firefox I can have one tab on one account and one tab on one contact and easily switch between them.  I wish the title on the tabs reflected the record being edited instead of just "Sage SalesLogix".
  5. The Back button is in my opinion easier to use than the one on the LAN client.
  6. The ability to bookmark or email links to specific records or (sometimes) groups.  Technically this should be possible in the LAN client using the slx:// links but Sage never pushed on that.
  7. The activity reminder is not as invasive.
  8. TABS. That one is pretty awesome. I can start from the list view, mid-click 10 tickets, and have them all open so I can compare them.
  9. And finally the #1 thing to love about the web client, NO NEED TO SYNC.

Well that is it for me.  Sadly there are still a few showstoppers that make it unpractical to use the web client in my daily work much as I would like to.  I am sure the situation will improve soon.  I am going to try and remember to revisit this post after each service pack to find out if the concerns have been addressed.

Comments
No Comments »
Categories
Saleslogix
Tags
SlxWeb
Comments rss Comments rss
Trackback Trackback

Categories

  • Experiments (4)
  • Interesting (1)
  • MSCRM (1)
  • Programming (60)
  • Rant (3)
  • Saleslogix (34)
  • Tricks (8)
  • Uncategorized (24)

Post History

  • 2010
    • January (3)
    • March (1)
  • 2009
    • March (2)
    • April (1)
    • May (3)
    • June (3)
    • July (1)
    • September (3)
    • October (2)
    • December (5)
  • 2008
    • January (9)
    • February (4)
    • March (9)
    • April (1)
    • May (5)
    • June (8)
    • July (1)
    • August (2)
    • September (1)
    • November (1)
    • December (3)
  • 2007
    • January (3)
    • February (7)
    • March (1)
    • April (3)
    • May (6)
    • June (2)
    • July (1)
    • August (2)
    • September (5)
    • October (3)
    • November (5)
    • December (4)
  • 2006
    • January (2)
    • September (1)
    • November (3)
    • December (4)
  • 2005
    • April (1)

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox