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

Saving an Attachment to Saleslogix

Nicolas Galler | June 4, 2008

This is an example of how much simpler, cleaner and more powerful the code in the new client is.  Here is a method that will take an arbitrary entity (well, arbitrary to a point – it has to be one of the "TALCO" entities) and write an attachment under it.

/// <summary>
/// Create a new attachment for the designated file.
/// </summary>
/// <param name="entityType">(Case sensitive) entity type that the attachment is going to be related to (eg Account, Opportunity)</param>
/// <param name="entityId">PK</param>
/// <param name="path">Absolute path to file to be attached.  
///  It will be copied to the attachment path.</param>
public object SaveAttachment(string entityType, object entityId, string path, string description)
{
    Type entityTypeActual = Type.GetType("Sage.Entity.Interfaces.I" + entityType + ",Sage.Entity.Interfaces");
    if (entityTypeActual == null)
        throw new InvalidOperationException("Invalid entity type " + entityType);
    object parentEntity = EntityFactory.GetById(entityTypeActual, entityId);
    if(parentEntity == null)
        throw new InvalidOperationException("Invalid entity id or type: " + entityType + "/" + entityId);
    IAttachment attachment = EntityFactory.Create<IAttachment>();
    switch (entityType)
    {
        case "Account":
            attachment.AccountId = (String)((IAccount)parentEntity).Id;
            break;
        case "Opportunity":
            attachment.AccountId = (String)((IOpportunity)parentEntity).Account.Id;
            attachment.OpportunityId = (String)((IOpportunity)parentEntity).Id;
            break;
        case "Contact":
            attachment.AccountId = (String)((IContact)parentEntity).Account.Id;
            attachment.ContactId = (String)((IContact)parentEntity).Id;
            break;
        case "Ticket":
            attachment.AccountId = (String)((ITicket)parentEntity).Account.Id;
            attachment.ContactId = (String)((ITicket)parentEntity).Contact.Id;
            attachment.TicketId = (String)((ITicket)parentEntity).Id;
            break;
        case "Lead":
            attachment.LeadId = (String)((ILead)parentEntity).Id;
            break;
        default:
            throw new InvalidOperationException("Unsupported entity type " + entityType);
    }

    attachment.AttachDate = DateTime.Now;
    attachment.Description = description ?? System.IO.Path.GetFileName(path);
    // save the attachment so that the Id property is populated
    attachment.Save();
    // copy the path to the attachment folder and save the attachment record
    // this will also populate the user
    attachment.UpdateFileAttachment(path);
    return attachment.Id;
}

And this is a unit test for it:

/// <summary>
///A test for SaveAttachment
///</summary>
[TestMethod()]
public void SaveAttachmentTest1()
{
    AttachmentWriter target = new AttachmentWriter();
    using (ISession sess = new SessionScopeWrapper())
    {
        IAccount acc = sess.CreateQuery("from Sage.SalesLogix.Entities.Account order by newid()").List<IAccount>()[0];
        string entityId = (string)acc.Id;
        string entityType = "Account";
        string path = "dynamicmethods.xml";
        string description = "dynamicmethods";
        object attachId = target.SaveAttachment(entityType, entityId, path, description);
        IAttachment attach = EntityFactory.GetById<IAttachment>(attachId);
        Assert.IsNotNull(attach);
        try
        {
            String attachPath = Path.Combine(Sage.SalesLogix.Attachment.Rules.GetAttachmentPath(),
                attach.FileName);
            Assert.IsTrue(File.Exists(attachPath), "File was not copied: " + attach.FileName);
            File.Delete(attachPath);
        }
        finally
        {
            attach.Delete();
        }
    }
}
Comments
2 Comments »
Categories
Programming, Saleslogix
Tags
Attachment, Saleslogix
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

Open a popup from your smart part

Nicolas Galler | January 28, 2008

Boo, hiss, popups! Web developers always hate popups. We hate them so much that we go to great length to fake them inside of the browser. And there are a lot of reasons for hating them, but there are also a few good use cases for them.

You can’t open a popup from the server side since it is, well, on the server; and you can’t use a <script> tag inside of a smart part, because it is rendered on an UpdatePanel, but you can still push out some script to be executed by the page via the ScriptManager control. Here is the code I use to navigate to a new web page via a popup:

ScriptManager.RegisterClientScriptBlock(this.Page, GetType(), "NavigateToUri",
    "window.open('" + uri + "', '_new', 'height:200,width:300,statusbar,menubar,resizable,titlebar,scrollbars');",
    true);

Note that if the URI comes from an external source (like, the database) you may want to sanitize it to avoid XSS attacks.

Comments
No Comments »
Categories
Programming
Tags
ASP.NET, Saleslogix, Slx Web
Comments rss Comments rss
Trackback Trackback

« Previous Entries

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