SlxGridHelper – Make the SlxDataGrid more convenient in our custom smart parts
Posted By: nicocrm on May 22nd, 2009 in Uncategorized
No Gravatar

There are a few things that I like to set on all my datagrids (double-click edit, single-click select, delete/add button). In an earlier post I had described how to tweak the quickform template so that every quickform containing a grid would receive these changes.  However there were a number of drawbacks to that approach, starting with the fact that it was rather tedious to implement and affected “stock” grids negatively.  I also found out that the limit of quickforms were very easy to hit and therefore ended up doing custom smartparts in most cases.  Therefore I decided to group this functionality in an external control that would be included on the custom smart part and modify the grid’s behavior using its public API.

The general principle is the same as the one used in the previous approach, but with the increased flexibility I wanted to implement the following features:

  • Ability to specify Add/Delete buttons, using control ids pointing to other objects in the form
  • Ability to have a grid’s title bar generated, optionally including Add/Delete buttons (useful when there are several grids on a form, or a grid and some other controls)
  • Optionally specify the databinding and parent/child relationship, so as to avoid having to put the datasource code on the smartpart itself (the code to bind a Saleslogix datagrid is rather noisy and not too pretty to look at)

The code for a typical grid… with an automatically generated toolbar:

<SSS:SlxGridHelper ID="grdSalesEntitiesHelper" runat="server" GridId="grdSalesEntities"
    GridTitle="Sales Entities" ShowAddButton="true" ShowDeleteButton="true" AutoBind="true">
    <DialogSpecs ActiveSmartPartID="SEOppSalesCreditForm" CenterDialog="true" 
     DialogHeight="300" DialogWidth="600" Title="Edit Sales Entity">
        <MyChildInsertInfo ChildEntityTypeName="Sage.Entity.Interfaces.ISEOppSalesCredit, Sage.Entity.Interfaces"
             ParentEntityTypeName="Sage.Entity.Interfaces.IOpportunity, Sage.Entity.Interfaces"
             ParentReferencePropertyName="Opportunity"
             ParentsCollectionPropertyName="SEOppSalesCredits" />
    </DialogSpecs>
</SSS:SlxGridHelper>
<SalesLogix:SlxGridView runat="server" ID="grdSalesEntities" GridLines="None"
AutoGenerateColumns="false" CellPadding="4" CssClass="datagrid" PagerStyle-CssClass="gridPager"
AlternatingRowStyle-CssClass="rowdk" RowStyle-CssClass="rowlt" SelectedRowStyle-CssClass="rowSelected" ShowEmptyTable="true" EnableViewState="false"
 ExpandableRows="True" ResizableColumns="True"  >
<Columns>
  <asp:ButtonField CommandName="Edit" DataTextField="SalesmanName" HeaderText="Salesman Name" />
  <asp:BoundField DataField="Adder" HeaderText="Adder"/>
  <asp:TemplateField HeaderText="Percentage">
  <ItemTemplate>
    <asp:Label runat="server" Text='<%# Bind("Percent", "{0:##%}") %>' ID="lblPercent" />
  </ItemTemplate>
  </asp:TemplateField>
  <asp:BoundField DataField="Title" HeaderText="Title" />
 </Columns>
</SalesLogix:SlxGridView>

And for a grid that is included on a tab (using the built-in toolbar – notice the “DeleteButtonId” and “AddButtonId”):

<SSS:SlxGridHelper ID="grdHelper" runat="server" GridId="grdLDC"
    AddButtonId="btnAdd" DeleteButtonId="btnDelete"
    GridTitle="" ShowAddButton="false" ShowDeleteButton="false" AutoBind="true">
    <DialogSpecs ActiveSmartPartID="SEOppLdcForm" CenterDialog="true" 
     DialogHeight="300" DialogWidth="600" Title="Edit LDC Account">
        <MyChildInsertInfo ChildEntityTypeName="Sage.Entity.Interfaces.ISEOppLDCAccount, Sage.Entity.Interfaces"
             ParentEntityTypeName="Sage.Entity.Interfaces.IOpportunity, Sage.Entity.Interfaces"
             ParentReferencePropertyName="Opportunity"
             ParentsCollectionPropertyName="SEOppLDCAccounts" />
    </DialogSpecs>
</SSS:SlxGridHelper>
<SalesLogix:SlxGridView runat="server" ID="grdLDC" GridLines="None" AllowSorting="true" AllowPaging="true"
AutoGenerateColumns="false" CellPadding="4" CssClass="datagrid" PagerStyle-CssClass="gridPager"
AlternatingRowStyle-CssClass="rowdk" RowStyle-CssClass="rowlt" SelectedRowStyle-CssClass="rowSelected" ShowEmptyTable="true" EnableViewState="false"
 ExpandableRows="True" ResizableColumns="True"  >
 <Columns>
    <asp:ButtonField CommandName="Edit" HeaderText="LDC Name" DataTextField="ProspectName" ButtonType="Link" SortExpression="ProspectName" />    
    <asp:BoundField HeaderText="City" DataField="City" SortExpression="City" />
    <asp:BoundField HeaderText="State" DataField="State" SortExpression="State" />    
    <asp:BoundField HeaderText="Zip" DataField="Postalcode" SortExpression="Postalcode" />
    <asp:BoundField HeaderText="Zone" DataField="Zone" SortExpression="Zone" />
 </Columns>
</SalesLogix:SlxGridView>
<SalesLogix:SmartPartToolsContainer runat="server" ID="toolbar" ToolbarLocation="right">
    <asp:ImageButton runat="server" ID="btnAdd" ImageUrl="ImageResource.axd?scope=global&type=Global_Images&key=plus_16x16"  
        AlternateText="Add LDC Account Number" ToolTip="Add LDC Account Number" />
    <asp:ImageButton runat="server" ID="btnImport" ImageUrl="ImageResource.axd?scope=global&type=Global_Images&key=Import_History_16x16"
        AlternateText="Import from Spreadsheet" ToolTip="Import from Spreadsheet" />
    <asp:ImageButton runat="server" ID="btnDelete" ImageUrl="ImageResource.axd?scope=global&type=Global_Images&key=Delete_16x16"  
        AlternateText="Remove LDC Account Number" ToolTip="Remove LDC Account Number" 
        OnClientClick="return confirm('Are you sure you wish to delete this LDC account number?');" />   
</SalesLogix:SmartPartToolsContainer>

 

There is no associated code-behind in either case (of course, if I wanted to customize the databinding, I would need to do that in the code behind).

The result looks like this (this is a grid with a “generated” toolbar):

AddlDetails

The code is simple – similar to what I used before in the quickform version.  I saved a copy here if interested feel free to rummage through it and keep what you want.


Tips & Tricks of the Web Client
Posted By: nicocrm on May 15th, 2009 in Programming, Saleslogix
No Gravatar

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

General Tips

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

Data Access

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

Lookups

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

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

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

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

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

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

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

Databinding

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

Using IChangedState to track before/after values of an entity
Posted By: nicocrm on May 11th, 2009 in Uncategorized
No Gravatar

This is a very nifty feature of the new Saleslogix platform that addresses the very common scenario of detecting when the user edited a field.  In the legacy client we often had to either run a SQL or manually save the old value to a global on the form – this was a rather tedious and dirty if you had to check a lot of properties.  In this new platform the change tracking is built-in – there are a few steps to it so it does not necessarily save in terms of line of code but it is less messy.

The basics are documented in the Saleslogix Web Client FAQ:

private static String GetOldMainPhoneValue(IAccount account)
{
    IChangedState accountState = account as IChangedState;
    if (accountState != null)
    {
        ChangeSet changes = accountState.GetChangedState();
        PropertyChange mainPhoneChange = changes.FindPropertyChange("MainPhone");
        
        if (mainPhoneChange != null)
        {
            return (string) mainPhoneChange.OldValue;
        }
    }
    return null;
}

However there are a few pitfalls.

First of all and not really related, but I found out your business rules would not execute if you specified it as a “Pre-Execute” or “Post-Execute” target but did not have a primary step on the rule.

Secondly, you have to know when this update information is available.  As far as I can tell this is available on the form handlers, as well as on the “OnBeforeUpdate” entity events, but not once the entity has been saved (e.g. on the OnAfterUpdate event).

Another point is that there is a slightly different syntax depending on the type of change to be tracked.  If it is a simple property change the above code (FindPropertyChange) works.  However if the change is on a related entity… for example opportunity.AccountManager… you have to use this syntax:

ChangeSet changes = changeState.GetChangedState();
EntityPropertyChange change = changes.FindMemberChange<EntityPropertyChange>("AccountManager");
string newUserId = (String) change.NewEntity.EntityId;
string oldUserId = (String)change.OldEntity.EntityId;

Unfortunately this does not work with simple properties… so you have to know which property you are dealing with in order to know how to examine the changes.