A bit of exploration with SalesLogix, Custom portals, and ASP.NET MVC
Posted By: nicocrm on July 4th, 2009 in Programming, Saleslogix
No Gravatar

Ever tried this menu?

image

It basically lets you set up a new deployment – that is, a way to export your defined entities, dynamic methods, and interfaces.  But you don’t get much when you try the “Portal Wizard” – the site it creates is almost empty, and because there will be no authentication set up, you can’t add pages to it right away either.  On the plus side, it gives you a good building base and is very flexible.

I decided to give it a try and combine it with a quick shot at ASP.NET MVC (which, in case you did not know, is an alternative to regular ASP.NET published by Microsoft – if you like the simplicity of Rails or Django you will recognize some of those frameworks in there).  I thought this might be a good option for some external sites that don’t need the full overhead of the regular Saleslogix web client.

As a little experimentation I decided to create a quick contact lookup site, it is actually slightly useful to me as the site is rudimentary enough to be used from a “dumb” phone (any phone that supports HTML) and as it does not have the long start up time of the regular web client it can be used to look up a contact basic info very quickly.

The only real trick was finding the right “modules” to configure in web.config.  I also had to enable the integrated pipeline (from IIS7) from ASP.NET MVC (technically you can use it with the classic pipeline but the URLs don’t look as good), but fortunately this had no impact on Saleslogix (as long as you make it 32 bit).  So here are the steps in (more or less) details.

The first step is obviously to download ASP.NET MVC from http://www.asp.net/mvc

Next, I started up Visual Studio and created a new ASP.NET MVC site.  This created a sample application with a few demo pages which I removed (remove everything under Views\Account and Views\Home, and remove the AccountController).  I did test it before removing to make sure it worked!

At the same time (actually right before) I created a new portal in the Application Architect (using the “Service Host” template, but I don’t think it really makes a difference) and deployed it.  I copied over my ASP.NET MVC site, keeping the following files from the Saleslogix portal:

  • hibernate.xml
  • log4net.config
  • ServiceHosts.xml
  • dynamicmethods.xml
  • application.xml
  • connection.config

Remember that those files will be re-generated every time we deploy the custom portal. The rest of the files come from the ASP.NET MVC template, we’ll eventually add those to the support files section of the portal so that we can re-deploy everything at once.

Next came the tricky part ;) I added the following modules to web.config (under system.webServer/modules – remember, we use the IIS7 section, not the IIS6 one):

<add name="ProcessModule" type="Sage.Platform.Application.UI.Web.AppManagerModule, Sage.Platform.Application.UI.Web" />
<!-- NHibernate Session scope -->
<add name="SessionScopeModule" type="Sage.Platform.Framework.SessionScopeWebModule, Sage.Platform" />

<add name="SlxAuthenticationResetModule" type="MvcApplication1.Utility.SlxAuthenticationResetModule, MvcApplication1"/>

The ProcessModule is probably the most important – it initializes the good old ApplicationContext.Current object.  SessionScopeModule works on maintaining an active NHibernate section for each web request, it is important for lazy-loading requests to work correctly (i.e. when you try to access myAccount.Contacts). The last one is not essential, it is a simple module I added to prevent drops when the session is lost (I am sure you have seen the “Authorization Token cannot be null” error a number of times – this module deals with it, does not work on the “regular” web client though because there are a few other things going on there).  While I was in there I also set up the authentication, we use a basic Forms authentication here:

<authentication mode="Forms">
    <forms loginUrl="~/Home/LogOn" timeout="2880" />
</authentication>

I went in global.asax and changed the default route to Home/Search as well (which does not exist yet):

routes.MapRoute(
  "Default",                                              // Route name
  "{controller}/{action}/{id}",                           // URL with parameters
  new { controller = "Home", action = "Search", id = "" }  // Parameter defaults
);

Now HomeController.cs is where most of the fun happens, starting with the LogOn function.  Here we must make sure that we validate the user and also save his credentials into the “token” that is going to be used to build connection strings.  Fortunately this is quite simple:

public ActionResult LogOn()
{
    return View();
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(String username, String password, string returnUrl)
{
    SLXWebAuthenticationProvider auth = (SLXWebAuthenticationProvider)ApplicationContext.Current.Services.Get<IAuthenticationProvider>();
    if (auth == null)
    {
        throw new InvalidOperationException("Can't access Authentication Provider");
    }
    auth.AuthenticateWithContext(username, password);
    try
    {
        using (TransactionScope tx = new TransactionScope())
        {
            IUser user = EntityFactory.GetRepository<IUser>().FindByProperty("UserName", username).First();
            // now this will only succeed if the connection was successful
            FormsAuthentication.SetAuthCookie(user.Id.ToString(), true);
        }
    }
    catch (Exception)
    {
        auth.Invalidate();
        ViewData.ModelState.AddModelError("message", "Authentication Failed");
        return View();
    }
    if (!String.IsNullOrEmpty(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return RedirectToAction("Search", "Home");
    }
}

The AcceptVerbs attribute on the second overload ensures that this is the function called when the form is posted, while the parameter-less overload is called on initial requests.  The “return View()” means return the view with the same name.  As for the view I just copied the one from Account/LogOn.aspx to the Home folder.  Note that the “Remember Me” box actually keeps you logged in here, not like the Remember Me box of the Saleslogix client.  It’s handy for a cell phone so you don’t have to retype your password every time.  That part only works with the “SlxAuthenticationResetModule” I mentioned above though.

The “Search” action (in HomeController.cs) is extremely simple but it does illustrate the access to the EntityFactory… the “Authorize” attribute ensures that they get to log in first.  We just get a (short) list of matching contact, stick it in the session, and send the control to the Contact/List action.  If there is only one match we redirect to the detail screen.  I only pass the Id, to make it simpler, but since it will be cached by NHibernate there is no performance hit.

[Authorize]
public ActionResult Search()
{
    return View();
}

[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Search(String search)
{
    Session["List"] = null;
    IList<IContact> contacts = FindContact(search);
    if (contacts.Count == 1)
    {
        return RedirectToAction("Details", "Contact", new { id = contacts.First().Id });
    }
    else if (contacts.Count > 1)
    {
        Session["List"] = contacts;
        return RedirectToAction("List", "Contact");
    }
    else
    {
        ViewData["Message"] = "No match!";
        return View();
    }
}

private IList<IContact> FindContact(string search)
{
    var repo = EntityFactory.GetRepository<IContact>();
    using (var session = new SessionScopeWrapper(false))
    {
        var query = session.CreateQuery("from Sage.SalesLogix.Entities.Contact where LastName like ?")
            .SetString(0, search + "%")
            .SetMaxResults(15);

        return query.List<IContact>();
    }
}

Add a view under Home/Search.aspx to make this complete.  This is the code I used for it:

    <% using (Html.BeginForm("Search", "Home")){ %>
    <p>
        Search: <%= Html.TextBox("search") %>
    </p>
    <% if (ViewData["Message"] != null){ %>
       <p>
        <%= ViewData["Message"] %>
       </p>
    <% } %>
    <p>
        <input type="submit" value="Search" />
    </p>
    <% }  %>

ContactController.cs is even simpler… for the List, we just pick it up from the session, and display it.  For the Details, we retrieve the contact by id.  For the sake of completeness here is the code:

[Authorize]
public ActionResult List()
{
    IList<IContact> list = Session["List"] as IList<IContact>;
    if (list == null)
        return RedirectToAction("Search", "Home");
    return View(list);
}

[Authorize]
public ActionResult Details(String id)
{
    IContact contact = EntityFactory.GetById<IContact>(id);
    if (contact == null)
        return RedirectToAction("List");
    return View(contact);
}

Now one very neat thing is the way the data binding works.  Try adding a view under contact and select “strongly typed view”.  Under the View Data Class enter “Sage.Entity.Interfaces.IContact” (you have to type it in as the dropdown will not list interfaces).  Then in the view you can use things like “Model.Mobile” – and the intellisense will show the auto-complete suggestion for you!  This is my details view… very basic of course:

 <h2><%= Html.Encode(Model.FullName) %></h2>

<fieldset>
    <legend>Fields</legend>
    <p>
        Work Phone:
        <%= Html.Encode(Model.WorkPhone) %>
    </p>
    <p>
        Mobile:
        <%= Html.Encode(Model.Mobile) %>
    </p>
    <p>
        Company:
        <%= Html.Encode(Model.AccountName) %>
    </p>
    <p>
        Email:
        <%= Html.Encode(Model.Email) %>
    </p>
</fieldset>
<% if (Session["List"] != null){ %>
<p>
    <%=Html.ActionLink("Back to List", "List")%>
</p>
<% } %>
<p>
    <%=Html.ActionLink("Back to Search", "Search", "Home") %>
</p>

And my list view (in List.aspx) – here for the type I entered “IEnumerable<Sage.Entity.Interfaces.IContact>”.  This can be entered after the fact under the “Page” directive also.  Here is the full listing:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
    Inherits="System.Web.Mvc.ViewPage<IEnumerable<Sage.Entity.Interfaces.IContact>>"
    ContentType="text/html" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    List
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <table border="1">
    <tr>
        <th>Name</th>
        <th>Company</th>
    </tr>
    <% foreach (var contact in Model){ %>
    <tr>
        <td><%= Html.ActionLink(contact.FullName, "Details", new{ id = contact.Id.ToString() }) %></td>
        <td><%= contact.AccountName %></td>
    </tr>
    <% } %>
    </table>
    <p>
        <%= Html.ActionLink("Back to Search", "Search", "Home") %>
    </p>
</asp:Content>

By the way, little detail here, don’t forget to add ContentType="text/html" otherwise some phones will return an “File format unknown” error (my Nokia did).  All done!  This is what it looks like on the phone:

image

Hey, nobody said it was going to be a work of art!

All in all an interesting little experiment.  I will probably come back and use that info in the near future as we have to do a small portal-type site outside of the web client.  One neat thing by the way is that this can also be used even if the customer is still on the LAN client.