Saleslogix bandwidth saver – store viewstate in Session
Posted By: nicocrm on September 12th, 2009 in Programming, Saleslogix
No Gravatar

As I try to optimize the bandwidth on the site using caching, compression etc it still remains very high.  There are substantial improvements on that front in the upcoming SP2 but that does not mean those of us stuck in the present are completely helpless.  If you use a tool such as fiddler to analyze the traffic on a postback (for example, when you change the account type – which does not actually result in any visible change on the UI) you may see something like (note, this is uncompressed):

Request Count:     1
Bytes Sent:     17,790
Bytes Received: 184,648

That’s a pretty big chunk – both on the way in and out.

For comparison here is gmail when I browse from message to message (uncompressed):

Request Count:     1
Bytes Sent:     1,295
Bytes Received: 7,521

Well part of it is just the nature of the ASP.NET UpdatePanel – they practically resend the whole page every time so it is bound to take a good bit more space.  But another culprit is the ViewState – this is something that ASP.NET uses to “remember” what is displayed on the browser (since fundamentally, the web is stateless – if you say you want to view the 3rd row in the grid, the server has to have some way to remember what this 3rd row is pointing to).  What happens is the server takes all (or most) of the values on the page, and their corresponding data sources, serializes them to text, and stick them in a hidden field on the page as something like this:

/wEPDwUENTM4MQ9kFgJmD2QWAgIBD2QWDgICDw8WAh4EVGV4dAUOTmljb2xhcyBHYWxsZXJkZAIGDw8WBB4IQ3NzQ2xhc3MFBk5hdkJhch4/wEPDwUENTM4MQ9kFgJmD2QWAgIBD2QWDgICDw8WAh4EVGc+bDAwJFRhYkNvbnRyb2wkZWxlb …  11000 more characters in between … 9nZE+NljKHHLWlju6N/3/hWmlJpeFXEXyFTQgICZGQCDA9kFgQCAQ9kFgJmD2QWAgIBD2QWAmYPZBYgAgEPDxYEHglNYXhMZW5ndGgCgAEfAAUZQ2VhWmlJpeFX

So you have 11 to 30 kb of “stuff” to pass back and forth on each and every post, not to mention the valuable CPU it takes to serialize and deserialize it.  Usually it is rather neat to have it on the browser rather than on the server because it means if your session is torn (for example because the web server restarted) you will keep this viewstate.  However this is rather moot on Saleslogix since the user is kicked out in that event anyway… so why not leave it on the server in the first place?  Turns out Microsoft thought of that too and they provided us with a way to store the ViewState in the session.  Saves the bandwidth and the CPU time to handle it (session values are stored “as-is” in memory so it is pretty fast).  There are posts describing the process here and here – I am including both because I took an in-between approach.  Once I did that I reduced the ViewState to this (it still needs a key to be able to look up the viewstate in the session):

/wEPZwUPOGNjMDFjZGYxNWZjYmViQpgJqyUrZohSJ3XG/uZnVdKp1Vk=

and the data transmitted on a simple postback to this:

Request Count:     1
Bytes Sent:     7,740
Bytes Received: 158,740

It yields a small but perceptible speed improvement on postbacks (the improvement is rather negligible on page changes though – when you switch from contact to account, for example).  One downside is it will require a bit more memory on the server side but even at an extra 600 kb per user (enough for 20 of the biggest viewstates) it is only a few hundred megs.

The steps were as follows:

public class SessionViewStatePageAdapter : PageAdapter
{
    public override System.Web.UI.PageStatePersister GetStatePersister()
    {
        return new SessionPageStatePersister(this.Page);
    }
}
  • Add an “App_Browser” folder to the site (it shows up as an option in Visual Studio) and drop a file “Default.browser” with this content:
<browsers>
    <browser refID="Default">
      <controlAdapters>
        <adapter controlType="System.Web.UI.Page" adapterType="SessionViewStatePageAdapter"/>
      </controlAdapters>
    </browser>
</browsers>
  • Add this tag to the web.config (under system.web), otherwise only part of the ViewState will be affected:
<browserCaps>
      <case>RequiresControlStateInSession=true</case>
</browserCaps>
  • In this state the server will get confused if the user opens more than 9 windows (not including the “in-page” popups) so I increased that to 20 by adding this under system.web in web.config:
<sessionPageState historySize="20"/>

All in all a decent improvement for a minimal effort.


Display user lookup as a dropdown (legacy-web style)
Posted By: nicocrm on September 1st, 2009 in Uncategorized
No Gravatar

Our users upgraded from 7.0 to 7.5 complained about the user lookup dialog – on 7.0 it used to be a dropdown, whereas on 7.5 it’s a popup similar to the LAN client.

image

Sorry for the chopped image, I had to hide the name and was too lazy to re-run on the eval db.

The problem is the popup takes a couple seconds to come up on IE7, sometimes more on slow machines or slow connections – it’s a bit irritating.  After a brief phase of denial (how dare they refuse the New and Improved way, etc) I set to work.  Had to struggle a bit with the combination of available properties but finally got it working.  Configuring lookup is definitely a big pain on the web client!  This is the winning combo:

  • Display Mode = DropDownList
  • Display Property = UserInfo.UserName
  • Lookup Entity Name = User
  • Lookup Binding Mode = Object
  • In the Lookup Pre Filters, add a filter on “Enabled” (make sure the property is populated as System.Boolean, and set OperatorCode to “=”) and filter value of “true”

The DropDown style lookups are a bit more picky on the parameters than the regular lookup, because they actually use NHibernate parameters instead of inline HQL.  In general I would say it is a good thing to use parameters, though in this case it makes it a bit harder for us to hack around the limitation of the control.

Data binding works fine, the only big problem I have found is that it sorts by first name instead of last name for some reason (which you can’t really see in the screen shot as I removed the last name but you can imagine).


Troubleshooting Reporting Server
Posted By: nicocrm on September 1st, 2009 in Uncategorized
No Gravatar

This post is an example of the tricks you can accomplish with Reflector as well as a reminder for me on how to do this.

If you have tried to set up a report server on the web client and run into any kind of trouble you may have noticed that there was very little diagnostic.  Yesterday I upgraded one of our customers from 7.2.2 to 7.5.1.  Most of it went smoothly (apart from some error in the downloaded installation files) but the reporting just would not work – Firefox just printed an obscure message “XML Parsing Error: no element found”, while Internet Explorer showed an empty page. 

image

Well actually they were both right – in lieu of a diagnostic message the report server page (SLXWebReportingServer.ashx) just did not return anything at all.  The first thing I did was make sure logging was enabled, and I edited log4net.config to make the name a bit more obvious (by default it just logs messages as “Service” – I changed it to “Saleslogix Web Reporting”).  Yet it still showed no message.  I had to get the report handler to print more diagnostic.  As it is a compiled assembly this cannot be done directly – however we can extract it using Reflector:

image

Paste the code in a file called SLXWebReportingServer.ashx in the slxwebrpt folder and add the handler directive at the top:

<% WebHandler language="C#" class="SLXWebReportingServer" %>

Then comment out the httpHandler directive in web.config and re-run the report (or you can just F5 the reporting window).  This will give a compilation error, of course, since we have not added any “using” directive.  Turn off customErrors in web.config (add a <customErrors mode=”off”/> under <system.web>) and add the missing using statements until you get the page to compile with the same error as the original one (there might be a way to get Reflector to generate the using statements, if so, i have not found it).  This is what I had to add for this one:

using System;
using System.Web;
using System.Web.SessionState;
using Interop.SLXWR;
using System.Runtime.InteropServices;
using log4net;
using Sage.SalesLogix.Reporting.Server;
using System.Data.OleDb;
using System.Data;
using System.Xml;
using System.IO;
using Sage.Platform;
using Sage.SalesLogix;

Create a logger object

public class SLXWebReportingServer: IHttpHandler, IRequiresSessionState
{
    private static readonly ILog LOG = 
        LogManager.GetLogger(typeof(SLXWebReportingServer));

And verify that it works:

    public void ProcessRequest(HttpContext context)
    {
        try
        {
LOG.Warn("Boo");

OK, now we are ready to work.  The first thing I should have done at this point was add a log statement in the top-level catch block, at the very end of the file:

        catch (Exception exception2)
        {
LOG.Warn("Error in top level try/catch", exception2);
            context.Response.StatusCode = 500;
            context.Response.StatusDescription = exception2.Message;
        }

It’s a good idea to first scan for trapped, unlogged exceptions and make sure they are reported.  However, I smartly dove in and started peppering the code with LOG statements to get an idea of where it was hung:

    private static bool GetConnectionString(string userName, string password, string timeZoneKey, out string connectionString, out string dataSource, out string errorMsg)
    {
LOG.Info("GetConnectionString - 1");
        object obj2;
        connectionString = null;
        dataSource = null;
        errorMsg = null;
        string physicalApplicationPath = HttpContext.Current.Request.PhysicalApplicationPath;
        if (string.IsNullOrEmpty(physicalApplicationPath))
        {
            errorMsg = "ERR_APPLICATIONPATH";
            return false;
        }
LOG.Info("GetConnectionString - 2");
        string path = Path.Combine(physicalApplicationPath, "connection.config");
        if (!File.Exists(path))
        {
            errorMsg = "ERR_CONNECTIONCONFIG_NOTFOUND";
            return false;
        }
LOG.Info("GetConnectionString - 3");
        XmlDocument document = new XmlDocument();
        try
        {
            document.Load(path);
        }
        catch (Exception exception)
        {
            errorMsg = string.Format("ERR_CONNECTIONCONFIG_LOAD", exception.Message);
            return false;
        }
LOG.Info("GetConnectionString - 4");

You get the idea.  Eventually I found the GetRWPassword call was not returning and that’s when I realized there was a blanket catch statement.  From there the error was obvious (I had not installed the SQL native client on the report server) and of course the quick way to find out would have been to install the SalesLogix client on the box – but it was an interesting exercise nonetheless, and could prove useful in other cases.

To recap:

  1. First step should be to install the Sales client and make sure you can log in
  2. Then, edit the logging options to make sure you are looking for the correct messages
  3. If all fails, you can always replace the default handler to add some diagnostic
  4. Don’t forget to put the default handler back when done! :)

This is the ashx file I used (for version 7.5.1 – on other versions you’d have to make sure you get the appropriate code using Reflector).