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

More random errors.

Nicolas Galler | January 13, 2010

A quick offering to the great Google god, in case you are running into the same issue:

  • “NullReferenceException” in the “ExtractValuesFromCell” method – this is caused by the “pseudo” cell added by the SlxGridView. To work around, turn off the “ExpandableRows” flag.
  • Blank page with a NullReferenceException in the LookupControl ClientConfiguration.From method: check the provider listed in connection.config. It needs to be spelled “SLXOLEDB”. If it is spelled out “SalesLogix OLEDB Connection Provider”, it will fail (not sure why but on 7.5.2 sometimes the connection is output in that format – I think, because I created the connection in AA, instead of logging into the Admin first)
  • Web client crashes when grid.Sort is called – do not call that method from the Sorting handler because it will cause infinite recursion (the Sorting handler can often be empty)
  • AA gives ArgumentNullException when Update Properties is clicked: this is an installation problem. Run a repair. In fact, if you get random errors from AA, and they are not specific to one project, running a repair should probably be the first corrective action.
Comments
No Comments »
Categories
Saleslogix
Comments rss Comments rss
Trackback Trackback

Using Direct SQL in Web Grids

Nicolas Galler | January 8, 2010

Another (thankfully smaller) post on the SalesLogix journal, detailing how to use straight SQL in a web grid. I use that a lot when I have a complex, read-only query (so much faster than cranking out the corresponding C# code to do it through the entity model), and I know a lot of SalesLogix dev are very familiar with SQL, so figured it would be useful.

The article is at Back to Basics – Using Direct SQL in Web Grids

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

How to refresh tabs, from the client side (SalesLogix 7.5.2)

Nicolas Galler | December 11, 2009

As part of the many performance enhancement brought by this latest service pack, tabs are no longer automatically refreshing when a dialog closes. I think we can all agree that the gained performance is worth it :) But this has a few consequences:

  • First of all, if you are using a quickform as an insert form, you are all good. Yes, this is one of these cases where using a quickform actually paid off! Congratulate yourself for the good choice.
  • If you are using a custom smart part processed on the server side, it is quite straightforward, but requires a manual addition of this little piece of code to your server side script (right before or after closing the dialog):

    PanelRefresh.RefreshTabWorkspace();
  • If you are using a client-side customization (and I do use those once in a while for interactive or time-consuming processes), it is only slightly more complex – you need to trigger a refresh from the client side. Assuming you are closing the dialog with this type of code:
    DialogWorkspace._dialog.close();

    In that case I had to use a slight subterfuge to get the panel to refresh correctly. I added a hidden (server-side) button on the page:

    <asp:Button runat="server" ID="btnProcessServer" CssClass="btnProcess_server" style="display: none" />

    And then in the code-behind:

    /// <summary>
    /// This is called by the Javascript when processing is complete.
    /// Close the dialog, and ensure tab is refreshed.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void btnProcessServer_Click(object sender, EventArgs e)
    {
        DialogService.CloseEventHappened(sender, e);
        PanelRefresh.RefreshTabWorkspace();
    }

    Finally I just had to replace my javascript with the following to cause a postback and have the dialog closed from the code-behind from the button:

    $(".btnProcess_Server").click();

    Note that I am using the CSS class instead of the id, as the id is going to be transformed a bit by ASP.NET. It’s a bit of a cheat. There might be a creative way to do it entirely from client side, using the window.TabControl object (a reference to the tab workspace), however experience taught me creatively discovering hidden Sage Javascript API is a good way to waste my afternoon (and also get my butt kicked during upgrades!)

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

Automatically importing default SalesLogix namespaces for C# code snippets

Nicolas Galler | December 8, 2009

When working on web client forms you may have seen this error:


e:\inetpub\wwwroot\PhysicianLiaison\SlxClient\SmartParts\Contact\ContactDetails.ascx(678): error CS0246: The type or namespace name 'IContact' could not be found (are you missing a using directive or an assembly reference?)

For example this happens when you use an unqualified reference in a C# code snippet, such as:


var myContact = (IContact)BindingSource.Current

You can’t add a “using” statement so the work around is to qualify all references:


var myContact = (Sage.Entity.Interfaces.IContact)BindingSource.Current

To save on the typing, you can add a default import to your web.config file. Simply do a search for “<pages>” and add the following under that tag (or if you don’t see a <pages> tag at all, add it under <system.web>):


<namespaces>
<add namespace="Sage.Entity.Interfaces"/>
</namespaces>

Of course this can be used with your custom namespaces as well. If you are using extension methods to define business rules (as explained in Easy Business Rules with Extension Methods) it will let you use those too.

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

Make sure you disable debugging in production!

Nicolas Galler | December 2, 2009

Here is a quickie… Several times I have made the mistake of shipping a web.config with debugging enabled (fortunately always caught it at the last minute so far). This has a horrible affect on performance because it prevents caching. It can also cause some serious memory issues under heavy load. So I made myself a big warning on the login page:

Big old warning

The code for it looks like this (in Login.aspx):

    </asp:Login>
    <asp:Label style="clear: both; display: block; width: 100%; text-align: center; color: red; font-size: 24pt" runat="server" id="lblDebugWarning"
                Text="Debugging Enabled in web.config - Set to False for Production" />
</asp:Content>

And under Page_Load, very simple:

    protected void Page_Load(object sender, EventArgs e)
    {
        lblDebugWarning.Visible = HttpContext.Current.IsDebuggingEnabled;
        System.Web.UI.WebControls.CheckBox rememberMe = (System.Web.UI.WebControls.CheckBox)slxLogin.Controls[0].FindControl("chkRememberMe");

By the way it slightly funks up the display for IE6 – but only when the label is actually displayed, so not a big deal.

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

Why I switched to Linux for Saleslogix Development

Nicolas Galler | December 2, 2009

This seemed like a no-brainer. SalesLogix is a Windows application, all the development tools run exclusively on Windows, and even though it does support a Linux database server (with Oracle) the vast majority of our customers are even running their database server on Windows. Nevertheless, Windows as a developer’s desktop has some serious issues (even more so for SalesLogix-specific development) and after realizing that I was spending most of my time inside of virtual machines anyway to work around 64-bit and IIS7 issues (and to take a break from Vista’s constant security nags), I decided to run a little experiment to see exactly how usable (or not) using Linux as a desktop for development of a Windows-based program would be. I loaded a copy of Ubuntu Linux on my workstation and started experimenting – I used the Linux desktop for email, web and editing, and used VirtualBox images for SalesLogix development.

The results? Well, they are in the title of the post! I don’t know if the Linux desktop will be a permanent thing on my home computer but it is definitely a big benefit on my work machine. It turns out that:

  • Installation and updates is quite a lot easier thanks to the built-in package manager – no need to look for, install and keep up to date 2 dozens third party programs because everything is included
  • The desktop has a few neat features that make it a perfect work environment – virtual desktops is the big one for me, it basically emulates additional monitors. The mouse focus is also nicer (you can get those effects with 3rd party programs and tweaks on Windows but not as well integrated). All in all the desktop is a bit more comfortable to me – could be because I was already quite familiar with it though.
  • A lot of neat and actually useful toys that come with the box. For example, there is a widget to which you can paste images to send them directly to an image sharing site – it’s great for posting screenshots on the SLX forums. Or a sticky note app (there is one in Windows 7 too but it is not terribly useful for me as it can’t sync between computers). Again, because they come with the system I do not need to spend additional time looking for them and keeping them up to date.
  • 32bit vs 64bit issues are non-existent – no “WoW64″ here. Oh, the sweet relief. I believe it was all worth it just to get rid of that nightmare.
  • Git. Git is not that slow on Windows, in fact it is a lot faster than the alternatives. But the first few times I ran it on Linux it happened so fast I thought there was a bug and it was not picking up my changes. I am not exaggerating – a local git clone of a Saleslogix repo goes down from 30 to 2 seconds. The integration with the merge tools is also better (a lot better). So now when I need to do a big merge I bring the repo to my Linux filesystem, do the merge, and send it back. Words fail to express how much more pleasant this is.
  • Warm fuzzy feeling from using an open source OS? Maybe :)

Now to be fair not everything is rosy… First of all be prepared for a fairly steep learning curve if you have no experience with Unix at all. To make things worse there are usually 2 ways to do most things – the “intuitive” way, through the UI, which takes a lot of clicks and is not always terribly reliable (though usually about the same as what it would be on Windows), and the “easy (not)” way, through the command line, which is very fast but requires you to know what you are doing. Even so, some operations require a bit of Googling and gentle coaxing – for example installing and configuring the NVidia driver for multiple monitors was not quite a point-and-click operation. A lot of the desktop functions are not extremely stable or polished – for example, the file browser crashes on me every few days, the UI stutters a bit when doing a disk-intensive operation (though it has no problem coping with the memory pressure of having 6 VMs running at a time, unlike Windows), Adobe Flash applications don’t always work fine, and because some of the fonts available on Windows are not present some web pages display incorrectly. And external devices are usually not “officially” supported – I have had good luck so far (I am actually able to customize all of my keyboard buttons which is better than what I could do in Windows, and my older scanner which does not have a Vista driver works fine on Linux) but I am sure esoteric USB devices or certain graphic cards might cause some issues. Well, these are the reasons I am still not quite sure I will keep it as a desktop OS for my home computer.

More importantly for work and development, while the integration with the Windows file shares is good, it is not perfect – sometimes I have to copy a file to my system, edit it, then copy it back. And I do miss Outlook (not that it is a fantastic program in itself – but the level of integration with Exchange is great). If I was using the Outlook calendar a lot I might be very unhappy – as it is I manage fine from my Blackberry.

My conclusion is not to encourage anyone to switch to Linux. Rather, an invitation to keep your mind open, as the ideal solution may not always be the obvious one!

Linux Desktop

PS: I opened an alternate blog at Nico’s Linux with the Linux-specific stuff, since it is not directly related to CRM.

Comments
No Comments »
Categories
Experiments, Saleslogix
Comments rss Comments rss
Trackback Trackback

Saleslogix bandwidth saver – store viewstate in Session

Nicolas Galler | September 12, 2009

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:

  • Created a page adapter called “SessionViewStatePageAdapter” under the App_Code folder – see http://msdn.microsoft.com/en-us/library/system.web.ui.sessionpagestatepersister.aspx for the code, mine looks like this:
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.

Comments
No Comments »
Categories
Programming, Saleslogix
Comments rss Comments rss
Trackback Trackback

A bit of exploration with SalesLogix, Custom portals, and ASP.NET MVC

Nicolas Galler | July 4, 2009

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.  If interested feel free to download the code.

Comments
3 Comments »
Categories
Programming, Saleslogix
Comments rss Comments rss
Trackback Trackback

Tips & Tricks of the Web Client

Nicolas Galler | May 15, 2009

This is a collection of random tricks, pitfalls etc that I am encountering on the current web client project. I will update within the next few weeks with other tidbits I find out.

General Tips

  1. Do not use quickforms.  Ever.  Seriously.  For real.  Well, there are exceptions:
    • A form with really trivial layout, no dynamically set controls, and no validation logic
    • When trying to figure out the basic syntax to declare a Saleslogix control
    • When making a trivial change to an existing form
    • If you have limited knowledge of HTML and really, really don’t want to learn: this it is NOT an exception because you will need to know HTML in order to troubleshoot the quickform!
  2. In case of an error, start with the event log.  Especially for lookups.
  3. Use CSS to your advantage when laying out the forms!  The code in quickform smartparts can afford to be sloppy and repetitive because it’s automatically generated, but we can’t! 
  4. In order to add a linked entity, use code like:
    ISEOppUtility oppUtility = EntityFactory.Create<ISEOppUtility>();
    // link to the parent entity ...
    oppUtility.Opportunity = parentEntity;
    // ... and add to the parent's collection.  The link is bidirectional.
    parentEntity.SEOppUtilities.Add(oppUtility);
    oppUtility.Save();
    // save the parent... in theory this should cascade and 
    // save the child entity... but that does not always work
    parentEntity.Save();
    // ensure that all views get refreshed - this is not always necessary
    PageWorkItem.Services.Get<IPanelRefreshService>().RefreshAll();
  5. Do not try to rename anything in the App Architect – it will mix stuff up. It is quicker and safer to do it on the backend XML instead.
  6. Run the web site on .NET 3.5 using the following steps:
    • Global replace of 1.0.61025.0 to 3.5.0.0 in web.config (do NOT replace in the other files)
    • Add "dependentAssembly" tag under runtime/assemblyBinding:
      <dependentAssembly>
              <assemblyIdentity name="System.Web.Extensions" culture="neutral" publicKeyToken="31bf3856ad364e35"/>
              <bindingRedirect oldVersion="1.0.61025.0"
                               newVersion="3.5.0.0"/>
            </dependentAssembly>
    • Use this for compilation tag (under system.web):
    •     <compilation debug="true" defaultLanguage="C#">
            <assemblies>
              <!--
              Cannot add System.Core - it conflicts with LinqBridge which is required by SLX
              <add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
              -->
              <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
              <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
              <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
            </assemblies>
          </compilation>
    • As you cannot add System.Core not all features will be available. Most importantly System.Linq.Expressions.
  7. Do not use <script> tags for JavaScript on smart parts because they will get lost after a postback. Use ScriptManager.RegisterClientScriptBlock instead. Unfortunately this means a large performance hit if the script is big as the client will have to download and parse it every time. Alternatively, disable the top-level UpdatePanel, and use smaller updatepanels to do the job (this will make the page a LOT more responsive on slow links but requires a bit of work to make sure everything is still getting refreshed as it should)

Data Access

  1. Events on property changes have 2 serious drawbacks:
    • “BeforeUpdate” events do not fire at all
    • “AfterUpdate” events only fire when the entity itself is saved, thus they are not terribly useful as you might as well put that code in the OnUpdate event of the entity itself. Also, because it is processed after the entity is actually saved, you cannot easily modify the entity at this point (you have to resave it, but because it is already inside of a Save call, it may cause NHibernate to crash)
  2. Events on an “extension” entity do not always trigger correctly on “cascading” save. Basically if you have a rule on ContactExt.BeforeUpdate, and you make a change to the ContactExt field, then call Account.Save, the changes get saved, but the event rule does not necessarily get called. It is a bit confusing, but you will recognize it when it happens.
  3. Do not Delete and Save an entity within the same transaction (this includes Save / Delete that are made as part of a cascade operation)

Lookups

  1. You can have a lookup exclude the entities that were already selected using code like:

    lueAddUtility.LookupExclusions = parentEntity.SEOppUtilities.ToArray();

  2. To invoke a lookup programmatically the following code works, sloppy as it may look:
    ScriptManager.RegisterClientScriptBlock(this, GetType(),
                "ShowLookup",
                // we need a slight delay here to give the lookup a chance to initialize
                "$(document).ready(function() { setTimeout(function() { " + lueAddUtility.ClientID + "_luobj.show() }, 500) })",
                true);

    It is also possible to set up a lookup as "Button only" which creates the lookup button… good for "associate a record" buttons.

  3. It is possible to use custom HQL in a lookup, by doing a PreFilter that points to a non-string property (this may well break in a later version).  EG (taking advantage of the fact that TYPE is defined as an enum and not a string, this creates a condition to select remote, network and concurrent users only):
    <SalesLogix:LookupPreFilter PropertyName="Type" CondOperator="Not Equal to" FilterValue="'' and User.Type in ('N', 'M', 'C')" />

    Note the “Not Equal to” bit is case sensitive.

  4. If adding a LookupPreFilter on a field that is not a string (including enums), you have to add the single quotes explicitely.
  5. I am not sure if you are supposed to use CondOperator or OperatorCode in the LookupPreFilters. The documentation seems to mention CondOperator, but Saleslogix uses OperatorCode… the possible values for “CondOperator” are in the help file, but in addition to the ones mentioned there one can also use “Not Equal to”.
  6. Lookups get cached the first time they are accessed… if the definition changes on the smart part, Log Off, and log back in. Closing IE or restarting IIS is not necessary. To some extent this may make it hard to modify lookups on the fly. Interesting tidbit: if LookupExclusion is not null then the cache won’t be used. So if you want to set up the LookupPreFilter dynamically you may have to make sure you set up a LookupExclusion (this may be an empty array). Will probably break in a later version.
  7. Lookups with a displaymode of DropDownList behave completely differently and may as well be considered different controls. None of the above comments apply to them.

Databinding

  1. Remember that the Saleslogix databinding works with events (i.e. when TextChanged fires is when the property will be populated on the entity). This has 2 important consequences:
    • You can’t bind to a property that does not have a corresponding change event (e.g. DropDownList.SelectedValue)
    • Within a Change handler, it is hard to know whether the properties have all been brought in yet or not. Say you want to change the CurrentEntity.Price when the lookup returns – if txtPrice fires a TextChanged event after the LookupResultValueChanged event did, then the property will be overwritten. To be safe it has to be written both to the entity and to the control.
  2. On the property itself there is a way to characterize the datatype (for some property types). For example for a “Double” type property there is a way to indicate that the property refers to a percentage. This is probably useful in some situation but other times it messes up with the rounding and the display. So I turned it off (a few years ago I wrote a little piece of Javascript to format percentages in textboxes and it still works pretty well for me).
  3. There is a client-side component for the databinding (responsible for undoing the changes when the user hits the close button, for example). When messing with the DOM be careful not to get this one confused (do not move controls too far afar in the document – if they get under a different “workspace” some of the bindings won’t work).
  4. It is possible for a smart part to interfere with the databinding of other smart parts. For example calling Page.DataBind is a NO-NO. If you notice that some values are getting cleared when they shouldn’t (often happens with the values that are loaded from ControlState, e.g. GridView.DataKeys), either check the other smartparts on the page or use a work around (e.g. instead of using grid.DataKeys we can use a CommandArgument).
Comments
1 Comment »
Categories
Programming, Saleslogix
Comments rss Comments rss
Trackback Trackback

1st Saleslogix .NET Extension

Nicolas Galler | September 17, 2008

Technically not the very first one as I have played with it before but this is the first case where it actually allows me to do something that would have been impossible without it.

One very useful resource was this page on SlxDeveloper: http://www.slxdeveloper.com/page.aspx?id=56.  All articles are very interesting but this one in particular is full of example and extremely useful.  Thanks so much to Ryan for putting this together.

The extension in question is relatively simple, it builds a logo dynamically and displays a preview in the client.  As there is no way to manipulate graphics in VBScript without the .NET extensions this would have required an external program or a custom COM component and distributing that to the remotes could have been a real hassle.

There were a few things to note although the explanations on the afore-mentioned articles were so good that I won’t repeat them here:

  • The .NET extension has to be compiled against the exact same version of Saleslogix that is going to be used on the client.  This implies that it will have to be relinked after an upgrade.  Otherwise, you won’t get a clear error message, but the call to Application.Managed.Run will fail.
  • The Saleslogix assemblies (Sage.SalesLogix.NetExtensions.Framework.dll and Sage.SalesLogix.NetExtensions.Core.dll) have to be registered in the GAC.  If they aren’t, you have to add them (either use gacutil, or simply drag/drop them to C:\Windows\Assembly in the explorer).  Normally this is done by the installer but apparently this sometimes fail.
  • The .NET extension manager does not run on 64bit OS.  So you have to register the extensions on a separate machine.
  • The .NET extension bundles are not terribly reliable… I think you are better off just registering them by hand on the production machine.
  • This is the code that I use to load my custom control and have it build and display the logo:
    handle = Application.Managed.Create("Barrel Logo Extension", _
        "SSSWorld.BarrelLogoExtension.SlxLoader")

    On Error Resume Next
    Set logo = Application.Managed.Run(handle, panLogoContainer.HWND)
    If Err.Number <> 0 Then
        MsgBox "Error loading logo control: " & Err.Description & vbCrLf & _
            "Please verify that you have the current Saleslogix service pack (v7.0.1)." & vbCrLf & _
            "If this error persists, please contact your Administrator.", _
            vbCritical, "Error"
        ModalResult = mrCancel
        Exit Sub
    End If
    logo.LoadBarrelLogo g_sFileName, g_sLine1, g_sLine2, g_sLine3, g_sLine4, g_sLine5

The “BaseRunnable” implementation (called SlxLoader) returns a ComVisible object that can then be manipulated by the VBScript.  First I tried having it return the UserControl directly but this would not go through (presumably COM did not know how to control it).  However you do not have to register the COM interface using regasm or the “Register for COM interop” checkbox – just having the type bear the ComVisible attribute is enough.  And the object in question holds a reference to the UserControl – this is the implementation of the LoadBarrelLogo method:

public void LoadBarrelLogo(String imgPath, String line1, String line2, String line3, String line4, String line5)
{
  _logo.LoadBarrelLogo(imgPath, line1, line2, line3, line4, line5);

}

and the Run method:

public override object Run(object[] args)
{
    if (args.Length != 1 || !(args[0] is Int32))
        throw new InvalidOperationException("Usage: SlxLoader.Run(hwnd)");

    IntPtr formHwnd = new IntPtr((int)args[0]);
    _logo = new BarrelLogo();
    Win32Util.SetParent(_logo.Handle, formHwnd);
    SSSWorld.Common.Win32Util.RECT formSize;
    if (Win32Util.GetWindowRect(formHwnd, out formSize))
    {
        _logo.Size = formSize.Size;
    }
    return this;
}

This code is explained in detail in the SlxDeveloper article.

These .NET extensions are a very nice tool to have.  There is definitely some room for improvement in the implementation, though sadly as the focus has shifted away from the network client it is unlikely that we will see any.  In most cases if it is possible to do it in VBScript instead it will be easier to manage in the long term.  But having the possibility to do more if absolutely needed is good. 

Comments
4 Comments »
Categories
Saleslogix
Comments rss Comments rss
Trackback Trackback

« Previous Entries

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