Programming, technology, and CRM – from a Belgian programmer exiled to Missouri
  • rss
  • Home
  • Soft Gallery
    • autosvnbackup.sh
    • VBScript Snippets
  • Contact Me
  • Welcome

Web Mailmerge on Vista

Nicolas Galler | July 2, 2008

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.

Comments
1 Comment »
Categories
Saleslogix
Tags
Saleslogix, SlxWeb, slxwebmm
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

Customize the QuickForm DataGrid (toolbar buttons and double-click to edit)

Nicolas Galler | May 19, 2008

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.

Comments
2 Comments »
Categories
Programming, Saleslogix
Tags
Saleslogix, SlxWeb
Comments rss Comments rss
Trackback Trackback

Saleslogix “Add to ad-hoc group” Smart Part

Nicolas Galler | March 26, 2008

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

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

Improve performance of SlxWeb – Compression, Caching

Nicolas Galler | March 21, 2008

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 asmx ashx
    • Also run this one (same thing for the deflate algo): cscript adsutil.vbs SET W3SVC/Filters/Compression/deflate/HcScriptFileExtensions asp aspx axd asmx ashx
    • 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.

ALSO, important note for SLX 7.5.2 and above. The extension for the mail merge is ashx so if you use the above recommendation it will get compressed which will break it (on IIS6 – no such problem on IIS7 as they use content type, rather than file extension, to decide what gets compressed). Here is one way to get around that:

  • Create a dummy “SLXMailMergeServer.ashx” on your web site
  • Set a property on it in IIS. For example the expiration header. Does not matter what – you just want to get that file to come into existence in the IIS metabase
  • Then run: cscript adsutil.vbs SET W3SVC/1/root/SlxClient/SLXMailMergeServer.ashx/DoDynamicCompression false (of course the exact path would depend on whether you are running on the default web site or not… you could use the metabase explorer too)
Comments
No Comments »
Categories
Saleslogix
Tags
Saleslogix, SlxWeb
Comments rss Comments rss
Trackback Trackback

On the Coolness of unit-testing Saleslogix

Nicolas Galler | March 20, 2008

I admit it, I am a unit-testing junkie. I think it all come down to my immense laziness – I will go to great length to avoid the extra work of having to manually test my programs.

Until now the options for unit-testing in Saleslogix were very limited. I did go through the effort of writing a few automated tests in VBScript but for the most part it was not really justifiable – it is hard to maintain the separation of concern essential to unit testing and usually if I go to the trouble of forcing the concepts of encapsulation and polymorphism into vbscript I end up introducing more bugs than what I catch with unit testing.

Not so with the New and Improved Saleslogix. I want to demonstrate how unit testing in the Saleslogix web client can make our work faster, easier, and help us produce better quality code through a simple problem I faced today on the Insert Opportunity screen – the “OnCreate” opportunity rule would crash with a NullReferenceException. This of course has a very selfish goal – the more people we have excited about the benefits of automated unit testing in Saleslogix, the more Sage will be inclined to make their code testable (and maybe encourage them to use automated testing themselves!)

You could troubleshoot the issue by copying the rule to your own assembly, sprinkle it with logging statements, associate it in the architect, redeploying the web site, try the form again, then try and figure out where the error is. Once you have compiled the rule into your own assembly you can actually also open it in the Visual Studio debugger which will save a lot of time but you still have to go through those build, deploy, login, and trigger steps. I don’t know about your machine but it takes me about 1 1/2 minute to do a full build and deploy, then the debugger has to compile the site to open it which also takes 2 to 3 minutes, after which it still takes 30 to 50 seconds to log into the site and get to the right page. By that time my coffee is getting cold. And if you want to make a change to the rule and try again, you have to restart from the beginning… argh!

Enter unit testing. When unit testing you will still be loading the NHibernate configuration, dynamic methods etc, but you won’t have the overhead of the web server, javascript, painting the screens, or even rendering any output – you are cutting straight to the code you need to test.

So getting back to my problem. I can prepare a tiny method in my test class containing just one line of code: EntityFactory.Create(), right click it and run the test (in this picture it is shown as a call to a builder class, but this is just a wrapper around the EntityFactory):

Of course I had set up the dynamic method to call up my assembly (same thing that you would normally do through the AA interface but since we are just testing right now it is quicker to do it directly):

This fails (of course – as expected) and gives me the traceback I needed. OK, line 145! Let’s set a breakpoint and re-run it (note that you would have the same thing if you opened the web site in the debugger – this is just a LOT faster):

Oh well, the option must not be set, let’s set it in SQL and re-run the test (if you were testing on the live system you would have to restart the web server to get it to re-read the options at this point):

That’s right, 5.14 seconds! How long does it take you to restart IIS and relog into the web client? And perhaps the best part – I can now keep the test in the collection and it will automatically catch any similar problem in the 7.2.3 upgrade.

Hope this will convince some of the power in unit testing. You will want to check this post on how to set your environment up for unit testing: Unit Testing SLX – 7.2.2 Update. If you need any help send me an email or a comment.

Comments
2 Comments »
Categories
Programming, Saleslogix
Tags
Saleslogix, SlxWeb, Unit Testing
Comments rss Comments rss
Trackback Trackback

Accessing Saleslogix Groups Programmatically (part 1)

Nicolas Galler | March 19, 2008

In a previous post I examined how to get access the entity data (basically the ORM layer, as well as the dynamic methods piece) using the Saleslogix assemblies from outside of the web client. Obviously this is vital for unit testing, but also has some interesting application for external application. In this next installment I would like to look at the Groups API. In addition to being useful in unit testing and external application I feel the Groups API is poorly documented so a bit of exploring would help.

First off remember that most of the group access is done via a COM component called GroupTranslator. The goal of that component (which I presume was written in Delphi) is to translate the Group blob stored within the database (in the Plugin table) to an XML description and vice versa. It is not terribly reliable and Sage is notoriously slow about releasing fixes for it, but it is what it is. For the general cases it is probably still better than rolling out your own translator.

Next take a look (with Reflector) at the API offered in the 7.2 client. GroupInfo is the main one – it is full of useful static methods for manipulating the groups. Unfortunately they are not documented and some of them look very buggy so we have to thread carefully when dealing with it. It also has a few instance methods but watch out – it makes heavy use of globals so I would avoid messing too much with several groups at the same time. Another one we have to deal with is GroupContext – this has some information about the group that the web user is currently using (sadly it has a few pitfalls as we will see below). Very often when you use a method to retrieve the group’s data it seems to set the current group in GroupContext (as a global). So watch out for that. Sometimes you have to break down and examine the group’s XML yourself (as returned by the group translator) but I prefer to avoid that – my hope is that eventually the GroupInfo API will be fixed to be more reliable. Here are a few of my favorites:

  • GroupInfo.GetGroupIdFromNameFamilyAndType – don’t you hate having to figuring that one out in SQL on the LAN client?
  • GroupInfo.GetGroupInfo – static method to build a group info object, knowing the plugin id.

  • GroupInfo.GetGroupDataReader – I looked at the code and I am pretty sure this won’t release the connection correctly, so I would stay away from that one for now (too bad, it sounds yummy, and does not have the global reliance of the next one)
  • GroupInfo.GetGroupDataTable – almost as good as GetGroupDataReader, and does not have the connection problem. Only works when paging is enabled which can only be done using the last 2 overloads. Be careful if you use those because they will mess with the current (global) group context (not sure what the exact effect would be).
  • GroupInfo.GetGroupKeyFieldIDs – not bad to get a group’s data. One of the rare data retrieval methods that doesn’t affect the current global group. Unfortunately this is currently broken so do not use it.
  • GroupInfo.GetGroupIDs – I am not sure what the difference in purpose is with the previous one. But anyway, this uses the GetGroupDataReader method, so it will leak connection – do not use.
  • GroupInfo.GetGroupList – gets you a list of groups for a specific entity. Watch out retrieving some of the properties like IsAdHoc – some are very very slow. So it will be easier to access the DB directly in most cases I think.
  • GroupInfo.AddAdHocGroupMember (and AddAdHocGroupMembers) – to add to an adhoc group (works fine)
  • GroupInfo.CreateAdhocGroup – create a new adhoc group (this works well)
  • GroupInfo.AddLookupCondition – to add a condition to a dynamic group (didn’t try it but it looks OK)
  • GroupInfo.SaveAsNewGroup – save group to database (should work fine)
  • GroupInfo.getGroupSQL – this is a private method but I just had to mention it anyway. For example to retrieve the “where” part of the SQL:

    MethodInfo method = typeof(GroupInfo).GetMethod("getGroupSQL",
        BindingFlags.Instance | BindingFlags.NonPublic);
    String sql = (String)method.Invoke(currentGroup,
       new object[] { "WHERE", currentGroup.GroupXML, false, 1, 1, null });

    Unlike the GroupInfo.GroupSQL property, this one actually works. The first parameter (where I put “WHERE”) is the part of the SQL that you want to retrieve. You can use WHERE, FROM, SELECT, ORDERBY. WHERE seems to expand all parameters, even things like :UserID. The second parameter is whether you want to do paging or not. Usually false. The next 2 parameters are related to paging, but make sure you do NOT set them to 0. Last parameter is the column to sort by, but this is ignored unless you are using paging. If you use “ALL” as the SQL part, you will get the whole SQL for the group, but none of the parameters won’t be expanded.

  • GroupInfo.GetGroupLayoutsNodes – you can use this to get the columns from the group.
  • GroupInfo.WhereSQL – SQL condition for the group. Equivalent to getGroupSQL(“WHERE”). Works well.
  • GroupInfo.FromSQL – the part after the FROM keyword. Equivalent to getGroupSQL(“FROM”). Works well.
  • GroupInfo.GroupSQL – access the actual group SQL (same as in the LAN client). Doesn’t return the condition correctly (always returns it as “1=2″).

As a practical example here is an ugly little wrapper class with a working “GetGroupEntityIds” method:

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Sage.SalesLogix.Client.GroupBuilder;
using System.Xml;
using Sage.Platform.Orm;
using System.Data;

namespace SSSWorld.Slx72.Utility
{
    /// <summary>
    /// Helper methods for groups.
    /// </summary>
    public class GroupHelper
    {
        /// <summary>
        /// Return all entity ids on that group.
        /// </summary>
        /// <param name="groupId"></param>
        /// <returns></returns>
        public static String[] GetGroupEntityIds(String groupId)
        {
            String sql = GetGroupKeysSQL(groupId);
            using (SessionScopeWrapper session = new SessionScopeWrapper())
            {
                using (IDbCommand command = session.Connection.CreateCommand())
                {
                    command.CommandText = sql;
                    using (IDataReader reader = command.ExecuteReader())
                    {
                        List<String> ids = new List<String>();
                        while (reader.Read())
                        {
                            ids.Add(reader.GetString(0));
                        }
                        return ids.ToArray();
                    }
                }
            }
        }

        /// <summary>
        /// Retrieve the full group SQL.
        /// </summary>
        /// <returns></returns>
        public static String GetGroupSQL(String groupId)
        {
            GroupInfo groupInfo = GroupInfo.GetGroupInfo(groupId);
            StringBuilder sqlBuilder = new StringBuilder();
            sqlBuilder.Append("SELECT ")
                .Append(GetGroupSQLPart(groupInfo, GroupSqlPart.SELECT));
            BuildGroupFromClause(sqlBuilder, groupInfo);
            return sqlBuilder.ToString();
        }

        /// <summary>
        /// Retrieve the SQL appropriate for reading the group entity ids.
        /// </summary>
        /// <returns></returns>
        public static String GetGroupKeysSQL(String groupId)
        {
            GroupInfo groupInfo = GroupInfo.GetGroupInfo(groupId);
            XmlNodeList layoutNodes = groupInfo.GetGroupLayoutNodes();
            XmlElement layoutNode = (XmlElement)layoutNodes[0].ParentNode;
            String mainTable = layoutNode.GetAttribute("maintable");
            StringBuilder sqlBuilder = new StringBuilder();
            sqlBuilder.Append("SELECT A1.")
                .Append(mainTable)
                .Append("ID ");
            BuildGroupFromClause(sqlBuilder, groupInfo);
            return sqlBuilder.ToString();
        }

        public static String GetGroupSQLPart(GroupInfo groupInfo, GroupSqlPart part)
        {
            MethodInfo getGroupSQL = typeof(GroupInfo).GetMethod("getGroupSQL", BindingFlags.Instance | BindingFlags.NonPublic);
            return (String)getGroupSQL.Invoke(groupInfo, new object[] { part.ToString(), groupInfo.GroupXML, false, 1, 1, null });
        }

        /// <summary>
        /// Which part of the SQL do you want to select
        /// </summary>
        public enum GroupSqlPart
        {
            /// <summary>
            /// After the WHERE (WHERE keyword not included)
            /// </summary>
            WHERE,
            /// <summary>
            /// After the ORDER BY (ORDER BY keyword not included)
            /// </summary>
            ORDERBY,
            /// <summary>
            /// After the SELECT (SELECT keyword not included)
            /// </summary>
            SELECT,
            /// <summary>
            /// After the FROM (FROM keyword not included)
            /// </summary>
            FROM
        }

        #region Private Methods

        /// <summary>
        /// Append the FROM and subsequent clauses to the SQL builder.
        /// </summary>
        /// <param name="sqlBuilder"></param>
        /// <param name="groupInfo"></param>
        private static void BuildGroupFromClause(StringBuilder sqlBuilder, GroupInfo groupInfo)
        {
            sqlBuilder.Append(" FROM ")
                .Append(groupInfo.FromSQL);
            String where = groupInfo.WhereSQL;
            if (!String.IsNullOrEmpty(where))
                sqlBuilder.Append(" WHERE ").Append(where);
            String orderBy = GetGroupSQLPart(groupInfo, GroupSqlPart.ORDERBY);
            if (!String.IsNullOrEmpty(orderBy))
                sqlBuilder.Append(" ORDER BY ").Append(orderBy);

        }

        #endregion
    }
}

Another problem with GroupInfo is that almost all of its methods will want to call GroupContext.GetGroupContext for one reason or another, and GetGroupContext is hard-wired to HttpContext, so not very testing-friendly. This can be fixed in IL though I have not bothered yet.

That’s it for now – this turned out to be a lot harder than I thought it would be. It was actually much harder than on the LAN client because of the poor (or rather, non-existent) documentation and the fact that most of the methods shipped do not actually work. I certainly do not want to turn this post into a rant, but I still have to mention how truly appalling that is. The good side of this coin is that we know the Saleslogix devs are hard at work on the next version and from what I have seen it will probably include a major overhaul of the group interface (and the API, presumably) which might explain why they are not focused on fixing this one. Next installment will be how to get to this stuff from outside of the web client but I thought this short overview of the API warranted a post by itself.

Comments
2 Comments »
Categories
Programming, Saleslogix
Tags
Saleslogix, SlxWeb
Comments rss Comments rss
Trackback Trackback

Find the hidden smart part

Nicolas Galler | March 18, 2008

How to programmatically find if a smart part is present on a page (for example, if the behavior of one smart part depends on whether another smart part is loaded… in my case I wanted to show a button on my smart part ONLY if some other custom smart part had been added to the page):

  • If the smart part you need to find is on any workspace but DialogWorkspace, you can use FindControl (or FindControlRecursive – google for the code). Remember the smart part will only show up if it is displayed in the particular mode (for example if it is a Detail mode smart part it won’t be anywhere in List mode)
  • If the smart part is on the DialogWorkspace it is a bit trickier. If it is not currently displayed, it won’t be there as a control, but instead it will be in the private variable _mySmartParts of the DialogWorkspace.

So here is my FindSmartPart function:

/// <summary>
/// Find a smart part under the specified work item.
/// If the smart part is not on the page, return null.
/// </summary>
/// <param name="parent"></param>
/// <param name="smartPartId"></param>
/// <returns></returns>
private Control FindSmartPart(UIWorkItem parent, String smartPartId)
{
    foreach (var ws in parent.Workspaces)
    {
        if (ws.Value is DialogWorkspace)
        {
            // in this case peek in _mySmartParts
            FieldInfo field = typeof(DialogWorkspace).GetField("_mySmartParts", BindingFlags.Instance | BindingFlags.NonPublic);
            if (field == null)
                throw new InvalidOperationException("Field _mySmartParts not found in DialogWorkspace");
            Dictionary<object, ISmartPartInfo> smartParts = (Dictionary<object, ISmartPartInfo>)field.GetValue(ws.Value);
            if (smartParts != null)
            {
                foreach (object smartPart in smartParts.Keys)
                {
                    Control c = smartPart as Control;
                    if (c != null && c.ID == smartPartId)
                        return c;
                }
            }
        }

        foreach (var smartPart in ws.Value.SmartParts)
        {
            Control c = (Control)smartPart;
            if (c.ID == smartPartId)
                return c;
        }
    }
    return null;
}

Well, nobody said it would be pretty – but, it gets the job done.

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

Upgrade SLX from 7.2.1 to 7.2.2 using Subversion

Nicolas Galler | March 18, 2008

As a follow up from Using Source Control with SLX 72, here is how Subversion allowed the upgrade from 7.2.1 to 7.2.2 without having to use the AA bundle installation process (well, having to use it only once):

  1. Start with a “vanilla” 7.2.1 source tree, and create a branch for it.
  2. Create a Vanilla-7.2.2 branch and apply all the 7.2.2 bundles for it.
  3. Now use those 2 branches to upgrade your custom source tree:
  4. Subversion will apply the differences and mark all problem files with a “conflict” symbol:. You need to review those manually to merge the changes. In most cases I just accepted the new version (the file marked as “right”)
  5. This takes care of the “Model” part. For the actual portal source files (the web.config etc) I just redirect the portal source files to the “Vanilla” version.

I can’t really comment on whether or not this is easier than upgrading using the bundler because I was never able to successfully do that (except for the vanilla database)!! But at least it was faster.

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

Categories

  • Dojo (1)
  • Experiments (4)
  • Force.com (2)
  • Interesting (1)
  • Javascript (3)
  • MSCRM (1)
  • Programming (63)
  • Rant (3)
  • Saleslogix (41)
  • Tricks (8)
  • Uncategorized (32)

Post History

  • 2011
    • January (3)
    • February (2)
    • March (1)
  • 2010
    • January (3)
    • March (3)
    • April (2)
    • August (2)
    • October (4)
    • November (1)
    • December (2)
  • 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