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

Code for the bulk edit grid view

Nicolas Galler | December 24, 2007

In follow up to my post on the bulk edit grid view, here is the code I used for the grid view. It should be noted that most of the code and the inspiration were directly lifted from this post.

The code is in 2 parts:

  1. MixableGridView, this is a very small addition to the grid view to add an event. I like using this and add my extensions to the grid view rather than inheriting every time from GridView since it makes it more modular.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Text;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using log4net;
    
    namespace SSSWorld.WebControls
    {
        /// <summary>
        /// Simple customization to the stock grid view to enable a few extra events.
        /// This is used by the Mixin controls.
        /// </summary>
        [ToolboxData("<{0}:MixableGridView runat=server></{0}:MixableGridView>")]
        public class MixableGridView : GridView
        {
            private static readonly ILog LOG = LogManager.GetLogger(typeof(MixableGridView));
    
            private static readonly object EventRowInitializing = new object();
            /// <summary>
            /// Raised right before the row is created.
            /// Last place changes can be made to the row before its child controls are
            /// instantiated.
            /// </summary>
            public event GridViewRowEventHandler RowInitializing
            {
                add
                {
                    base.Events.AddHandler(EventRowInitializing, value);
                }
                remove
                {
                    base.Events.RemoveHandler(EventRowInitializing, value);
                }
            }
    
            protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
            {
                GridViewRowEventHandler handler = (GridViewRowEventHandler)base.Events[EventRowInitializing];
                if (handler != null)
                    handler(this, new GridViewRowEventArgs(row));
                base.InitializeRow(row, fields);
            }
    
        }
    }
  2. BulkEditGridViewMixin is the actual extension that enables the bulk-editing.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Text;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using log4net;
    
    namespace SSSWorld.WebControls
    {
        /// <summary>
        /// Mixin allowing edit of all rows within a gridview at the same time.
        /// Note that this will be sub-optimal with a "connected" data source
        /// (eg a SqlDataSource) - should be used with EntityDataSource for best
        /// results.
        /// </summary>
        public class BulkEditGridViewMixin : UserControl
        {
            private static readonly ILog LOG = LogManager.GetLogger(typeof(BulkEditGridViewMixin));
            private bool _inDataBinding = false;
            private bool _saved = false;
    
            /// <summary>
            /// Id of the grid for which to enable the bulk edit.
            /// Must be a MixableGridView instance.
            /// </summary>
            public String GridId
            {
                get { return ViewState["GridId"] as String; }
                set { ViewState["GridId"] = value; }
            }
    
            private MixableGridView Grid
            {
                get
                {
                    MixableGridView grid = NamingContainer.FindControl(GridId) as MixableGridView;
                    if (grid == null)
                    {
                        throw new ArgumentException("Invalid GridId parameter - must point to a MixableGridView");
                    }
                    return grid;
                }
            }
    
            /// <summary>
            /// Initialize the event handlers for the grid.
            /// </summary>
            /// <param name="e"></param>
            protected override void OnInit(EventArgs e)
            {
                base.OnInit(e);
    
                Grid.RowInitializing += new GridViewRowEventHandler(Grid_RowInitializing);
                Grid.RowUpdated += new GridViewUpdatedEventHandler(Grid_RowUpdated);
                Grid.DataBinding += new EventHandler(Grid_DataBinding);
                Grid.PageIndexChanging += new GridViewPageEventHandler(Grid_PageIndexChanging);
            }
    
            /// <summary>
            /// Ensure we save the edits right before changing the page.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void Grid_PageIndexChanging(object sender, GridViewPageEventArgs e)
            {
                if(!e.Cancel)
                    SaveGrid();
            }
    
            /// <summary>
            /// Save the grid's data before refreshing it.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void Grid_DataBinding(object sender, EventArgs e)
            {
                if (!_inDataBinding)
                {
                    _inDataBinding = true;
                    SaveGrid();
                    _inDataBinding = false;
                }
            }
    
            /// <summary>
            /// Ensure we keep the rows in edit mode.
            /// This also prevents the DataBinding event from re-firing inside
            /// of SaveGrid.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            void Grid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
            {
                e.KeepInEditMode = true;
            }
    
            /// <summary>
            /// Force rows to edit mode.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void Grid_RowInitializing(object sender, GridViewRowEventArgs e)
            {
                if(e.Row.RowType == DataControlRowType.DataRow)
                    e.Row.RowState |= DataControlRowState.Edit;
            }
    
            /// <summary>
            /// Commit grid data to the data source.
            /// This is automatically when the grid refreshes its data to ensure the
            /// edits are not lost eg when paging.  But it may also have to be called
            /// explicitely eg when the form's data needs to be committed to the database.
            /// eg when paging.
            /// </summary>
            public void SaveGrid()
            {
                if (_saved || Grid.Rows.Count == 0)
                    return;
                try
                {
                    LOG.Debug("Updating grid data: " + Grid.Rows.Count);
                    for (int i = 0; i < Grid.Rows.Count; i++)
                    {
                        if (Grid.Rows[i].RowType == DataControlRowType.DataRow)
                        {
                            Grid.UpdateRow(i, false);
                        }
                    }
                    _saved = true;
                }
                catch (Exception x)
                {
                    LOG.Warn("Error in SaveGrid", x);
                    throw;
                }
            }
        }
    }

Finally, here is how you would use it in an aspx page:

            <sss:MixableGridView runat="server" DataSourceID="dsReturnProducts" ID="grdReturnProducts" GridLines="None"
                AutoGenerateColumns="false" CellPadding="4" CssClass="datagrid" PagerStyle-CssClass="gridPager"
                AlternatingRowStyle-CssClass="rowdk" RowStyle-CssClass="rowlt" SelectedRowStyle-CssClass="rowSelected"
                AllowPaging="True" PageSize="10"
                EnableViewState="false"  DataKeyNames="_AccountProductId" >
                <EmptyDataTemplate>
                    (no product selected)
                </EmptyDataTemplate>
                <Columns>
                    <asp:BoundField DataField="ProductName" ReadOnly="true" HeaderText="Product Name" />
                    <asp:BoundField DataField="SKU" ReadOnly="true" HeaderText="SKU" />
                    <asp:BoundField DataField="AssetCode" ReadOnly="true" HeaderText="Date Code" />
                    <asp:BoundField DataField="NoOfBoxes" HeaderText="No. Boxes" />
                    <asp:BoundField DataField="TotalWeight" HeaderText="Total Weight" />
                </Columns>
            </sss:MixableGridView>
            <sss:BulkEditGridViewMixin ID="grdReturnProductsEdit" GridId="grdReturnProducts" runat="server" />

            <sss:SlxEntityDataSource  EntityDataSourceProperty="ReceivedProducts" ID="dsReturnProducts"
                runat="server"/>

There isn’t much to do in the code behind, but you may have to manually call the SaveGrid() method sometimes (depending on when you need the data to be available vs when you databind the grid). I just call it before doing my final Save on the form.

You can use any datasource control instead of the SlxEntityDataSource. But remember that if the datasource is “connected” (I mean, if its Update method actually sends an update command to the database, regardless of whether there is any change from the original or not) the performance may be pretty bad.

Comments
No Comments »
Categories
Programming
Tags
ASP.NET
Comments rss Comments rss
Trackback Trackback

SLX EntityBoundSmartPart lifecycle

Nicolas Galler | December 6, 2007

I added some logging statements to figure out what the lifecycle of user controls implementing SLX smart parts was and how it meshed with the standard ASP.NET events. This is what I came up with. SLX-specific events are in italic – the rest are standard ASP.NET (I skipped a few of the ASP.NET ones).

When the smart part is not displayed (eg an undisplayed dialog), none of the SLX stuff fires, but the standard ASP.NET still does:

  • OnInit
  • OnLoad
  • OnUnload

EntityContext.GetEntity() is available at all time but is of the type that is bound to the main form – not necessarily the same as the smartpart’s.

The first time it is displayed and bound:

  • OnInit
  • OnLoad
  • OnAddEntityBindings
  • OnLoadCurrentEntity starts
  • OnCurrentEntitySet
  • OnLoadCurrentEntity returns
  • MyDialogOpening
  • OnPreRender
  • OnFormBound (which is actually called from OnPreRender)
  • OnUnload

Again, EntityContext.GetEntity() is available but is of the type of the main form until OnCurrentEntitySet.

On postbacks:

  • OnAddEntityBindings
  • OnInit
  • OnLoad Starts
  • OnLoadCurrentEntity starts
  • OnCurrentEntitySet
  • OnLoadCurrentEntity returns
  • OnLoad Returns
  • Control postback events
  • OnPreRender
  • OnFormBound (which is actually called from OnPreRender)
  • OnUnload

EntityContext.GetEntity() is available and of the correct type.

OnFormBound is not a bad place to hook data binding stuff but it only fires from the PreRender which is a bit late sometimes. OnCurrentEntitySet executes before the bound data is saved to the entity (therefore before any SLX property change handler fires) so it is usually not great either.

Comments
1 Comment »
Categories
Saleslogix
Tags
Favorites
Comments rss Comments rss
Trackback Trackback

Slx 7.2 Web does not save (all) state between postbacks

Nicolas Galler | December 3, 2007

Reminder for me to post on this in the morning… The “EntityStateService” will not save changes to objects that are more than 1 level deep in a relationship. For example: if you have an opportunity, the opportunity has quotes, the quote has line items. You are on an insert form for an opportunity and you let the user add some quotes. Fine. Now you try adding line items to the quotes. Remember the form is bound to an opportunity (a transient object). The changes to the opp are saved. The changes to the quotes are saved. The changes to the line items are not saved. I am not talking about saving to DB – I am talking about saving the state to the session between postbacks.

The code (EntityStateGraphVisitor) uses a “shallow” visitor pattern to persist the changes to the session (I suppose, to avoid ending up saving the entire DB in the session), so this fails to save the changes 2 levels deep.

Update on this: SLX is preparing a system in the next update to allow for saving deeper entities – no details yet on how it will work but hopefully it will help alleviate the above problem.

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

Unit Testing SLX

Nicolas Galler | December 2, 2007

While the framework provided by Sage makes it quite convenient to develop the web application, there is little to no documentation on what is needed to run it “stand-alone” as is needed for unit-testing. This post describes a scenario for a simple unit test I want to run against one of the business rules I designed for my entity. In addition to being useful in its own right for writing unit tests, it helped me understand a lot of the inner workings of the framework.

The specification for the business rule is quite simple: if a Salesorder entity associated with an Opportunity has its status changed to Won, the Opportunity should be changed to Won, and all other sales orders on the opportunity need to have their status changed to “Else Won”. Here is the code for the unit test in its unmodified form:

IOpportunity opportunity = EntityFactory.Create<IOpportunity>();
ISalesorder quote = new SalesOrderBuilder().Build(opportunity);
ISalesorder secondQuote = new SalesOrderBuilder().Build(opportunity);

try
{
    opportunity.Save();

    quote.Status = "Won";
    quote.SaveForm();

    Assert.AreEqual(quote.Status, "Won",
        "Quote status should save as Won");
    Assert.AreEqual(opportunity.Status, "Closed - Won",
        "Opportunity status should change to Closed - Won");
    Assert.AreEqual(secondQuote.Status, "Else Won",
        "Other Quote status should save as Else Won");
}
finally
{
    try
    {
        opportunity.Delete();
    }
    catch (Exception x)
    {
        LOG.Warn("Error cleaning up opportunity", x);
    }
}

If we try running the above code it will crash as soon as we attempt to access the EntityFactory, for it requires the whole ApplicationContext to be instantiated and configured. With much trial and error I found out the following:

  • ApplicationContext.Initialize(appId) will initialize the application. appId is an arbitrary string. This returns a WorkItem object, which we can then use through the rest of the tests. It also initializes ApplicationContext.Current, which is good because some methods are hard-wired to use that global instead of the parameters (grrr). In the web client this is done by the AppManagerModule, a registered HttpModule.
  • Each of the pieces of the app that need a configuration file (for example NHibernate and the Dynamic Method invocation piece) will by default read it under All Users\Application Data\…….Configuration\Application\<appId>\… Of course, that doesn’t work for testing, and it can be redirected, but for each module that you want reading its configuration from another spot you will have to write something like this (for NHibernate, in this case):
    ConfigurationManager configManager =
      workItem.Services.Get();
    ReflectionConfigurationTypeInfo typeInfo =
      new ReflectionConfigurationTypeInfo(typeof(HibernateConfiguration));
    typeInfo.ConfigurationSourceType = typeof(FileConfigurationSource);
    configManager.RegisterConfigurationType(typeInfo);
    

    This will cause the code to read the file from Configuration\Application\<appId>\, relative to the current directory. For the web client they have a slightly different strategy. The code that reads the configuration detects when it is run on the web (HttpContext.Current != null) and in that case uses Server.MapPath to resolve the configuration path.

  • At that point you will have to add a bunch of the Saleslogix assemblies to the references for the app. I was half tempted to add the entire web bin folder but I just stuck it out and re-ran the application until I got all assemblies. In addition to the regular list you will need the Sage.Entity.Interfaces and Sage.SalesLogix.Entities assemblies that are specific to the project, and the following SLX assemblies:
    • Castle.DynamicProxy.dll
    • NHibernate.Caches.SysCache.dll
    • Sage.SalesLogix.Activity.dll
    • Sage.SalesLogix.BusinessRules.dll
    • Sage.SalesLogix.NHibernate.dll
    • Sage.SalesLogix.Plugins.dll
    • Sage.SalesLogix.SpeedSearch.dll
  • Saving or querying NHibernate will not work until a “DataService” which can open new connections is defined. I created a “TestDataService” class which reads the connection string defined in app config and serves it. Then, it needs to be registered like so:

    workItem.Services.AddNew(typeof(TestDataService), typeof(IDataService));

  • Some other services may be required by the code inside of the business rules. The following should probably be added:

    workItem.Services.AddNew(typeof(MockUserService), typeof(IUserService));
    workItem.Services.AddNew(typeof(WebUserOptionsService), typeof(IUserOptionsService));

    The MockUserService is defined to hard-code Admin as Userid/Username (note that the username is really the user code, not the user name). The default implementation returns the Windows user name instead. Not sure what the reasoning was there, but anyway, it won’t work.

  • To simulate a databound context we need to emulate what the page does (look at Account.aspx for example) and set up the entity context (which also registers the entity history service if it hasn’t already been done):

    _workItem.Services.AddNew(typeof(EntityFactoryContextService), typeof(IEntityContextService));

  • Problem in NHibernate: connection is closed… Happens in SessionImpl, Cascades.Cascade (inside of DoSave). You can use this.connectionManager.connection.State to debug. I have not yet found the solution to this one, but it only happens when saving so it is not critical for testing. My gut feeling is this happens inside of the ID generator.

Here is my complete (current) TestSetup class, feel free to rip, you may have to adjust a few of the SSSWorld.Common dependencies. I will come back and update this post as needed if I find new things:

using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using Sage.Platform.Application;
using Sage.Platform.Configuration;
using Sage.Platform.Data;
using System.Text.RegularExpressions;
using SSSWorld.Common;
using System.IO;
using log4net;
using System.Data.OleDb;
using System.Data;
using Sage.Platform.DynamicMethod;
using Sage.SalesLogix.Security;
using Sage.SalesLogix.Web;
using Sage.SalesLogix;
using Sage.Platform.Security;
using System.Threading;
using System.Security.Principal;
using System.Diagnostics;
using Sage.Platform.Services;

namespace SSSWorld.Cablofil.SlxPlatform.BL.UnitTest
{
    [SetUpFixture]
    public class TestSetup
    {
        private WorkItem _workItem = null;
        private static readonly ILog LOG = LogManager.GetLogger(typeof(TestSetup));

        [SetUp]
        public void SetupTest()
        {
            log4net.Config.XmlConfigurator.Configure();
            //Globals.Initialize();

            try
            {
                _workItem = ApplicationContext.Initialize("Test");
                _workItem.Services.AddNew(typeof(TestDataService), typeof(IDataService));
                ConfigurationManager configManager = _workItem.Services.Get<ConfigurationManager>();
                ReflectionConfigurationTypeInfo typeInfo = new ReflectionConfigurationTypeInfo(typeof(HibernateConfiguration));
                typeInfo.ConfigurationSourceType = typeof(FileConfigurationSource);
                configManager.RegisterConfigurationType(typeInfo);
                typeInfo = new ReflectionConfigurationTypeInfo(typeof(DynamicMethodConfiguration));
                typeInfo.ConfigurationSourceType = typeof(FileConfigurationSource);
                configManager.RegisterConfigurationType(typeInfo);

                //Thread.CurrentPrincipal = new SLXWindowsPrincipal(User.GetById("ADMIN"), WindowsIdentity.GetCurrent());
                _workItem.Services.AddNew(typeof(MockUserService), typeof(IUserService));
                _workItem.Services.AddNew(typeof(WebUserOptionsService), typeof(IUserOptionsService));
                _workItem.Services.AddNew(typeof(EntityFactoryContextService), typeof(IEntityContextService));
            }
            catch (Exception x)
            {
                LOG.Warn("Test setup failed", x);
                throw;
            }
        }

        [TearDown]
        public void TearDown()
        {
            if (_workItem != null)
            {
                try
                {
                    _workItem.Dispose();
                }
                catch (Exception x)
                {
                    LOG.Warn("Error trying to dispose work item", x);
                }
                try
                {
                    ApplicationContext.Shutdown();
                }
                catch (Exception x)
                {
                    LOG.Warn("Error shutting down app context", x);
                }
            }
        }

        public class MockUserService : SLXUserService
        {
            #region IUserService Members

            public override string UserId
            {
                get
                {
                    return "ADMIN";
                }
            }

            public override string UserName
            {
                get
                {
                    return "Admin";
                }
            }

            #endregion
        }

        /// <summary>
        /// This data service accesses the connection string named "Saleslogix" defined in app.config.
        /// Username and password must be specified in the connection string.
        /// </summary>
        public class TestDataService : IDataService
        {
            private OleDbConnection _connection;

            #region IDataService Members

            public string Database
            {
                get
                {
                    Regex rx = new Regex("Initial Catalog= *([^ ;])");
                    Match m = rx.Match(GetConnectionString());
                    if (m.Success)
                        return m.Groups[1].Value;
                    throw new InvalidOperationException("Could not extract database name from connection string");
                }
            }

            public string Server
            {
                get
                {
                    Regex rx = new Regex("Data Source= *([^ ;])");
                    Match m = rx.Match(GetConnectionString());
                    if (m.Success)
                        return m.Groups[1].Value;
                    throw new InvalidOperationException("Could not extract server from connection string");
                }
            }

            public System.Data.IDbConnection GetConnection()
            {
                lock (this)
                {
                    if (this._connection == null)
                    {
                        this._connection = new OleDbConnection(this.GetConnectionString());
                    }
                    else if (this._connection.ConnectionString.CompareTo(this.GetConnectionString()) != 0)
                    {
                        if (this._connection.State == ConnectionState.Open)
                        {
                            this._connection.Close();
                        }
                        this._connection.Dispose();
                        this._connection = new OleDbConnection(this.GetConnectionString());
                    }
                }
                return this._connection;
            }

            public string GetConnectionString()
            {
                return MyConfiguration.Instance.GetConnectionString("Saleslogix");
            }

            #endregion
        }
    }
}

This should be useful also for running external applications accessing the Saleslogix database, for example Windows services. It is not practical yet because of the lack of transaction support but should be in the near future (I plan to come back to this and fix the current issue with saving an entity at that time!)

Comments
1 Comment »
Categories
Saleslogix
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