Web Mailmerge on Vista
Posted By: nicocrm on July 2nd, 2008 in Saleslogix
No Gravatar

Compared to the instructions at http://customerfx.com/pages/crmdeveloper/2008/02/21/solving-thttprequestthread-errors-with-activemail-in-saleslogix-web.aspx there are a few different steps to get the SLX Mailmerge piece to work on a Vista web server (IIS7).

  • Configure the web manager, same as on 2003 or XP (same as the instructions on Ryan’s post)
  • Create the /mmserver virtual directory and point it to C:\PF\SLX\Web Components\mmserver (same as before).  Make sure you create it as an Application and select the correct type of Application Pool (classic pipeline, 32bit enabled if running on Vista64)

image

  • Under mmserver click "Handler Mappings" and enable "ISAPI-dll":

image

image

  • On the server configuration (click on the server name), select "ISAPI and CGI Restrictions" and add an entry for the slxwebmm.dll:

image

image

Update (2008-08-23): Same instructions work with Windows Server 2008. We had to make sure that the mmserver and the SlxClient applications were both running in the same application pool for some reason.


Using Saleslogix Web
Posted By: nicocrm on June 2nd, 2008 in Saleslogix
No Gravatar

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.


Customize the QuickForm DataGrid (toolbar buttons and double-click to edit)
Posted By: nicocrm on May 19th, 2008 in Programming, Saleslogix
No Gravatar

Note (updated on 2009/05/22): do not use this. It is somewhat interesting as an example of how to mess with the web client internals but is too brittle for production code. It will also make your upgrade harder and make it harder for other devs to understand your code. Later I will make a post about how to achieve this same behavior using an unobtrusive, external control that respects the grid’s public API.

If there is one thing that can be said about the datagrid used in the Saleslogix Web Client, it is that it is ugly.  And clunky to use.  OK that makes 2 things, but they both had to be said!  Where is the nice "Add/Edit/Delete" menu that we have on the network client grid?  Instead you have the "Edit" column which is part of the basic ASP.NET datagrid.  Yuk.

Anyway, supposedly there is a revamp in the next version, so I don’t want to spend a major amount of time customizing the current one, but meanwhile I have to have something slightly more usable.  My goals are as follows:

  • Add an "Add" button within the caption of the datagrid (otherwise you have to put it on top of it)
  • Add a double-click action to edit an item
  • Add a "Delete" button within the caption of the datagrid

The Add/Delete buttons will let me emulate a datagrid toolbar which will be useful when the datagrid is embedded within a form, as opposed to being the only control on a tab.

The double-click action is just nicer/better looking than the edit column without being too hard to code.  If I had more time I would try and integrate a third-party control like Telerik or make use of the YUI datagrid but we can’t spend forever on this one.  Another nicety would have been the ability to integrate a control list within the datagrid but again, I got stuck on that one and decided not to waste more time.

Step 1: Add a "ShowAddButton" property to the QFDataGrid

This is not the most straightforward process because the code is pretty closed up, but here is how I did it:

  • Fired up ILDASM, opened the "Sage.SalesLogix.QuickForms.QFControls" assembly, and dumped it to an IL file
  • Edited the IL file and added my property… it looks a bit scary but in reality I just copy/pasted from the "ExpandableRows" property and changed the names:
  .field private bool _showAddButton

  .method public hidebysig specialname instance bool
          get_ShowAddButton() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldfld      bool Sage.SalesLogix.QuickForms.QFControls.QFDataGrid::_showAddButton
    IL_0006:  ret
  } // end of method QFDataGrid::get_ShowAddButton

  .method public hidebysig specialname instance void
          set_ShowAddButton(bool 'value') cil managed
  {
    // Code size       19 (0x13)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldarg.1
    IL_0002:  stfld      bool Sage.SalesLogix.QuickForms.QFControls.QFDataGrid::_showAddButton
    IL_0007:  ldarg.0
    IL_0008:  ldstr      "ShowAddButton"
    IL_000d:  callvirt   instance void [Sage.Platform.QuickForms]Sage.Platform.QuickForms.Controls.QuickFormsControlBase::NotifyPropertyChanged(string)
    IL_0012:  ret
  } // end of method QFDataGrid::set_ShowAddButton

  .property instance bool ShowAddButton()
  {
    .custom instance void [System]System.ComponentModel.BindableAttribute::.ctor(bool) = ( 01 00 00 00 00 )
    .custom instance void Sage.SalesLogix.QuickForms.QFControls.Localization.SRCategoryAttribute::.ctor(string) = ( 01 00 11 43 41 54 45 47 4F 52 59 5F 42 45 48 41
                                                                                                                    56 49 4F 52 00 00 )
    .set instance void Sage.SalesLogix.QuickForms.QFControls.QFDataGrid::set_ShowAddButton(bool)
    .get instance bool Sage.SalesLogix.QuickForms.QFControls.QFDataGrid::get_ShowAddButton()
  } // end of property 
  • Compiled using ilasm /dll Sage.SalesLogix.QuickForms.QFControls.il
  • Copied the resulting DLL to the PF\Saleslogix\Architect\Saleslogix directory (backup the existing one just in case!)
  • Restarted AA and admired my new property:

ShowAddButton Property

Step 2: Customize the template to show my add button

In the Model\QuickForms\Web you can edit the QFDataGrid.WebControlRenderingTemplate.vm file which controls how the datagrid is rendered to a web form.  Of course, this will all be invalidated by an upgrade etc but we are just playing here.

  • Add the code to show the button (do a search for "mainContentHeader" and replace that entire <div> with the following <table>):
#if((${qfcontrol.Caption} != "") && ($qfcontrol.Visible == true))
<table class="mainContentHeaderTable">
  <tr>
  <td>
    <asp:Label runat="server" Text="<%$ resources: grdFamily.Caption %>" ></asp:Label>
  </td>
  <td class="mainContentHeaderToolsRight">
  #if($qfcontrol.ShowAddButton)
      <asp:ImageButton runat="server" AlternateText="Add Record" id="${qfcontrol.ControlId}_btnAdd"
        ImageUrl="$generator.getImageResourceURL("Plus_16x16")"
        OnClick="${qfcontrol.ControlId}_btnAdd_Click" Text="Add"/>
  #end
  </td>
  </tr>
</table>
#end
  • Add the code for the button handler (I stole it from the "InsertChildAction" template).  Note that in order for this to work you will need to have added an Edit column (see next section for more on that).  This can be anywhere within the server script of the same template:
#if($qfcontrol.ShowAddButton)
protected void ${qfcontrol.ControlId}_btnAdd_Click(object sender, EventArgs e)
{
  if (DialogService != null)
  {
    DialogService.SetSpecs(${editcolumn.DialogSpecs.Top}, ${editcolumn.DialogSpecs.Left},
        ${editcolumn.DialogSpecs.Height}, ${editcolumn.DialogSpecs.Width},
      "${editcolumn.DialogSpecs.SmartPart}",
      #if($editcolumn.DialogSpecs.TitleOverride != "")
        GetLocalResourceObject("${editcolumn.DialogSpecs.ResourceKey}.DialogTitleOverride").ToString()
      #else
        string.Empty
      #end,
      ${editcolumn.DialogSpecs.CenterDialog.ToString().ToLower()});

    Type entityType = typeof(${qfcontrol.QuickFormDefinition.DefaultNamespace}.${qfcontrol.QuickFormDefinition.EntityTypeName});
    Type childType = typeof(#if($qfcontrol.BoundEntityTypeName != "")
        ${qfcontrol.BoundEntityTypeName}
      #else
        ${editcolumn.DialogSpecs.GetQualifiedEntityType()}
      #end );

    DialogService.EntityType = childType;
    // note that the "SetChildIsertInfo" is not a typo.  Well, it is, but not here.
    DialogService.SetChildIsertInfo(
      childType, entityType,
      // we'll assume that the parent relationship is simply the parent type name... eg Contact -> Account
      childType.GetProperty(entityType.Name.Substring(1)),
      entityType.GetProperty("${qfcontrol.BoundCollectionPropertyName}"));
    DialogService.ShowDialog();
  }
}
#end
  • We get something like this:

image

It is not a beauty, that’s for sure.  But that’s what we get with the built-in dialog service, so if the users can live with the rest of the web app surely they can live with this.

Step 3: the Edit button

The edit button is a bit easier because there is no need to add a separate property – we can just piggyback on the "HasEditColumn" property so we only have to modify the template file.

I added this code in the "RowDataBound" method:

#if($qfcontrol.HasEditColumn)
  if(e.Row.RowType == DataControlRowType.DataRow)
  {
    e.Row.Attributes.Add("ondblclick",
      Page.ClientScript.GetPostBackEventReference(${qfcontrol.ControlID}, "Edit$" + e.Row.RowIndex.ToString()));
  }
#end

And optionally add this to comment out the code that creates the edit column (since we don’t need it anymore…):

#macro(doEditCol $col)
## #if(!$IsPrintView && !$qfcontrol.RenderVertical)<asp:ButtonField CommandName="Edit"
##  #if($col.Text != "")Text="<%$ resources: ${qfcontrol.ControlId}.${col.ColumnId}.Text %>"#end
##  #if($col.DataField != "")DataTextField="${col.DataField}"#end
##  #if($col.MultiCurrencyDependent)AccessibleHeaderText="MultiCurrencyDependent"#end
##  #addCommon($col) >
##      #addStyle($col)
##  </asp:ButtonField>
## #end
#end

So you just add your edit column (same as normal) and this cause will call it to be invoked on double click instead of click of the column itself.

Step 4: adding a "Selected" handler and delete button

In order for the Delete button to work we’ll have to be able to select a row.  Of course you need to do that without a postback otherwise it will be agonizingly slow, but this is not too bad.

Since this is going to be used only when the Delete button is shown I added a bit of logic to control that and combined the 2 (technically would be a bit nicer to keep them decoupled but I am getting tired):

  • Handler in the server code (in the RowDataBound handler):
#if($showDeleteButton)
    e.Row.Attributes.Add("onclick",
      "${qfcontrol.ControlID}_selectGridRow(this, " + e.Row.RowIndex.ToString() + ")");
#end
  • In the toolbar (next to the Add button code):
#if($qfcontrol.HasDeleteColumn)
    #set($showDeleteButton = true)
    <asp:ImageButton runat="server" AlternateText="Delete Selected" id="${qfcontrol.ControlId}_btnDelete"
      ImageUrl="$generator.getImageResourceURL("Delete_16x16")" UseSubmitBehavior="False"
      OnClientClick="return ${qfcontrol.ControlID}_confirmDelete();"
      OnClick="${qfcontrol.ControlId}_btnDelete_Click" />
#end
  • Change the doDeleteCol macro:
#macro(doDeleteCol $col)
  #if(!$showDeleteButton && !$IsPrintView && !$qfcontrol.RenderVertical)
    <asp:ButtonField CommandName="Delete"
    #if($col.Text != "")Text="<%$ resources: ${qfcontrol.ControlId}.${col.ColumnId}.Text %>" #end
    #if($col.DataField != "")DataTextField="${col.DataField}" #end
    #if($col.MultiCurrencyDependent)AccessibleHeaderText="MultiCurrencyDependent"#end
    #addCommon($col) >
      #addStyle($col)
    </asp:ButtonField>
  #end
#end
  • Add a hidden field to store the selected value, and a handler to toggle it:
#if($showDeleteButton)
<script type="text/javascript">
// supporting script for the one-click select needed by the delete button
function ${qfcontrol.ControlID}_selectGridRow(row, rowIndex){
  var hid = $get("<%=${qfcontrol.ControlID}_hidSelectedId.ClientID%>");
  if(/rowSelected/.test(row.className)){
    hid.value = "";
    hid.selectedRow = null;
  } else {
    if(hid.selectedRow)
      hid.selectedRow.className = hid.selectedRow.className.replace(/rowSelected/, "");
    row.className += " rowSelected";
    hid.selectedRow = row;
    hid.value = rowIndex;
  }
}

function ${qfcontrol.ControlID}_confirmDelete(){
  if(!$get('<%=${qfcontrol.ControlID}_hidSelectedId.ClientID%>').value){
    alert('Please select a row to delete first.');
    return false
  }
  return confirm('Are you sure you wish to delete this record?');
}
</script>
<asp:HiddenField runat="server" id="${qfcontrol.ControlID}_hidSelectedId"/>
#end
  • And finally (phew), add the server code handler next to the add button handler:
#if($showDeleteButton)
// this retrieves the selected grid index from the hidden field and deletes the corresponding record.
// we assume the user has already been prompted for confirmation on the client side.
protected void ${qfcontrol.ControlId}_btnDelete_Click(object sender, EventArgs e)
{
  String childId = (String)${qfcontrol.ControlID}.DataKeys[Int32.Parse(${qfcontrol.ControlID}_hidSelectedId.Value)].Value;
  ${qfcontrol.BoundEntityTypeName} childEntity = #if($qfcontrol.DataKeyNames != "Id")
    (${qfcontrol.BoundEntityTypeName})Sage.Platform.EntityFactory.GetByCompositeId(typeof($qfcontrol.BoundEntityTypeName), "${qfcontrol.DataKeyNames}".Split(','), id.Split(','));
  #else
    Sage.Platform.EntityFactory.GetById<${qfcontrol.BoundEntityTypeName}>(childId);
  #end
  if(childEntity != null){
    ${qfcontrol.QuickFormDefinition.DefaultNamespace}.${qfcontrol.QuickFormDefinition.EntityTypeName} mainentity =
      this.BindingSource.Current as
        ${qfcontrol.QuickFormDefinition.DefaultNamespace}.${qfcontrol.QuickFormDefinition.EntityTypeName};
    mainentity.${qfcontrol.BoundCollectionPropertyName}.Remove(childEntity);
    if((childEntity.PersistentState & Sage.Platform.Orm.Interfaces.PersistentState.New) <= 0)
            {
      childEntity.Delete();
            }
  }
}
#end

Final result

Grid Image

Conclusion

All in all I think this is a good example of how to customize the stock controls.  However since you are still at the mercy of the dialog service there isn’t that much to be gained, especially in light of the amount of work (and the fact that you have to hack it up in the IL which is never all that fun).  If you look at the double-click action alone though this is a pretty big usability improvement and very easy to implement, so it might be worth just doing that part?  Not to mention that it doesn’t require you getting your hands into the IL grease.

Another thing that became (even more) evident to me while developing this is how frustrating the development with QuickForms is.  The feedback cycle is SO long between the time you make a tweak on your form and the time you can actually see it in the web client that it is very, very hard to bear.  Not to mention the number of time AA crashed on me or failed to deploy the content without giving me any error.  There is a lot of work to be done there and in the meantime it may be quicker to simply do it as custom smart parts.

I do like the Velocity templates.  They are primitive but simple and effective. I can’t say I am a fan of programming in notepad though – I think Visual Studio has spoiled me.

I believe another approach could be used to add our own custom controls to the control selection list in AA, which may be a better option for future maintenance (and maybe less development headache since we can move a lot of the work from the template to the custom control). 

And a final note…

Despite the presentation this is not intended to be a step by step guide on “how to get this in your datagrid”. First of all I doubt many will be willing to modify the IL. I also glanced over a few details, and I made a few more changes on the production system to make things smoother. This is more of a “look this CAN be done but omg it is painful” type of post. But if you are really interested in the finer details feel free to contact me.


Saleslogix "Add to ad-hoc group" Smart Part
Posted By: nicocrm on March 26th, 2008 in Programming, Saleslogix
No Gravatar

As you may know the “add to group” functionality is not currently implemented in the web client. There is a way to create a new ad-hoc group by selecting records from an existing group but no way to add records to an existing group (not to mention that the interface is a bit hard to use). In our case this was a crucial piece because the customer wanted to rely on the ability to add records to the “SyncSalesLogix” group to have them picked up by the Lotus Notes sync. Fortunately there is enough functionality in the API to build it ourselves. I created an “Add To Adhoc Group” smart part and uploaded it to the MSDN Code Gallery in case it is of interest to anyone else.

It should work with all entities for which the LookupView component works, though I only tested it with Accounts, Contacts and Opportunities. It is available as both source code and bundle-based installation and released under the open source Microsoft Public License (which is actually the only one available for MSDN Code Gallery).

One thing I should mention – it does not work very well for the Admin user. So make sure you test it out with a regular user.


Screenshot of the Add To Adhoc Group view


Improve performance of SlxWeb – Compression, Caching
Posted By: nicocrm on March 21st, 2008 in Saleslogix
No Gravatar

You can achieve a substantial improvement of the performance of SlxWeb by making sure the static data gets cached, and the compressible data gets compressed. There are some instructions on how to do that in the Saleslogix documentation but unfortunately they are incomplete and inaccurate so here are a few steps you want to make sure you take:

  • Obvious first step – make sure the <compilation> tag in web.config has debug=false (which it is by default, but we often turn it to true while developing). Leaving it to true will turn off some of the caching options.
  • Enable compression in IIS – in addition to the steps outlined in the doc (right click on “Web Sites”, go to Service, check “Compress static files” and “Compress application files”), run the following:
    • Start a command prompt and go to \inetpub\adminscripts
    • To ensure aspx and axd (web resources) are compressed, and to ensure the DLL aren’t (which would mess up mail merge), run (on one line):cscript adsutil.vbs SET W3SVC/Filters/Compression/gzip/HcScriptFileExtensions asp aspx axd
    • Also run this one (same thing for the deflate algo): cscript adsutil.vbs SET W3SVC/Filters/Compression/deflate/HcScriptFileExtensions asp aspx axd
    • To ensure Javascript and css are compressed, run: cscript adsutil.vbs SET W3SVC/Filters/Compression/gzip/HcFileExtensions js css htm html txt
  • Go to the properties of the “jscript”, “css” and “images” directory, go to Http Headers, turn on the content expiration
  • Restart IIS. Test with fiddler or a network capture tool to make sure it is working.

I should mention that these apply to IIS 6 only. Thankfully I have only had to set up SlxWeb on one Windows 2000 server so far.