Programming, technology, and CRM – from a Belgian programmer exiled to Missouri
  • rss
  • Home
  • 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

Bulk Edit Grid View

Nicolas Galler | November 23, 2007

As planned I have implemented the bulk edit mode for the grid view.

This is very inspired from this post from Matt Dotson, in fact the only difference is that it will save automatically on every page refresh instead of relying on a save button – this allows it to work in conjunction with paging and also not be affected when postbacks occur from other controls.

Compared to his article I had to implement these additional event handlers:

  1. Grid.DataBinding: I call SaveGrid here to ensure the data gets saved before it is refreshed. This needs to be protected with a lock because UpdateRow may cause databinding to be performed as well
  2. Grid.RowUpdated: set the KeepInEditMode to true – this prevents the grid from trying to rebind (so this should remove the lock requirement from the previous handler, but I kept it in to be on the safe side)
  3. Grid.RowInitializing: this is an event I added to the grid, because otherwise there is no way to slip something into InitializeRow without actually deriving from the grid (my strategy with the grid view is to define feature “mix-in” modules that can be added to a stock (or in this case, “almost” stock) grid view. The problem if you derive from GridView every time is it becomes hard to combine those features… i.e. a classic problem of inheritance vs composition. Unfortunate that ASP.NET favors the former, but that is for another post)
  4. Grid.PageIndexChanging: call SaveGrid here otherwise the data will be muffed when the user changes pages

The SaveGrid implementation is a bit simpler, since my DataSource control is completely disconnected so I can afford to call Update pretty often… for this reason I did not bother with keeping a “dirtyRows” list and instead update every single row in the datagrid.

In the end, it gives a pretty transparent experience to the user, something like this:

Comments
4 Comments »
Categories
Programming
Comments rss Comments rss
Trackback Trackback

Slx 72 Web Windows Authentication Explained

Nicolas Galler | November 22, 2007

If you read over the installation instructions to enable the Windows authentication for the 7.2 web client, you will see that there is a fairly lengthy setup process – you need to setspn this and that, configure your users to use Windows authentication, and set up the Web server to run as a domain admin, no less. You know when I read that I could not believe they would actually recommend running IIS – seems like the most obvious point of entry for any attacker – as a Domain Administrator, but I suppose it is not that big of a step from their previous recommendation to run it as a Local Administrator. I may be weird but I like this stuff to run as either Network Service or some other reasonably lowly trusted user. So I had to peek at the implementation to see if it was really necessary and how to get around it if it was.

Now in general under ASP.NET enabling Windows authentication by itself is a pretty easy task. What is a bit more difficult is getting it to coexist with the regular forms authentication, so that some users will be directed to the Windows login page, some to the forms login page, and they will be accessing the same application. In Saleslogix they have a special HTTP module called MixedModeSecurityModule – it will intercept requests to the /Windows.aspx page and hijack the forms authentication process at that point. Basically:

  1. User sends request to /Windows.aspx
  2. IIS sends a 401 response to get the browser to pass credentials. There is a bit of trickery here – 2 HTTP modules around the forms authentication module, one to hide the 401 status and replace it with a 200, and the other one to restore the 401 status so it is sent to the browser (otherwise, the forms authentication module will catch the whole thing and redirect to login page)
  3. Browser sends credential. As a side note I had a bit of trouble getting IE7 to send that automatically, if there is a dot in the site’s name it will assume it is in the internet zone and not pass them
  4. MixedModeSecurityModule handles the FormsAuthentication “Authenticate” event and retrieves the SID using the LogonUserIdentity property of the request, connects to the database (more on this below) and checks whether the SID passed is associated with a user.
  5. At this point it retrieves the user logon and password, decrypts the password, forms the connection string, and generates the FormsAuthentication cookie – the user is now properly set up.

No need for Domain Admin rights, right? Network Service will be enough since it will be able to identify the domain users.

There is one iffy step: the one where the module connects to the database to retrieve the user info. This is the step where the web site should use impersonation to connect to the Saleslogix server. Saleslogix would have you configure the whole web site to run as a privileged user, and, I guess that would work, IF the user was also enabled to log into Saleslogix – pretty radical, though. Seems like we have 2 options to get around it:

  1. Use an actual user for the connection string that will check the Windows Authentication. I tried that one, hard-coding the credentials for Admin, and it worked – so I know that is possible. The credentials are also stored somewhere in the registry (lightly encrypted) if the Legacy web components have been configured (which is required for the mail merge anyway)
  2. Use impersonation/delegation to pass the credentials of the user logging into the web site. This is kind of nicer because it doesn’t require us to dig for password, one drawback is the computer will have to be trusted for delegation… This is likely to be tough to get configured at customer’s sites so I am not going to look into it any further.

Either way, we have to replace the stock MixedModeSecurityModule class, and replace the GetUserPass method so that it will be able to connect to the database. The easiest way to make the change would be to paste the code from Reflector and add User Id and Password parameters to OleDbConnectionBuilder – so basically only 2 lines of code. The rest of the stuff looks a bit crusty (it seems like they tried a lot of different methods before settling on that one and forgot to clean up afterward) but it does work.

Finally, here are a few links that deal with the Mixed Mode authentication:

  • ASP.NET Mixed Mode Authentication
    By Paul Glavich
  • Mixed Mode Authentication (Ayende)
  • An article from Paul Wilson about it – uses a different approach, which I think looks a lot cleaner – doesn’t require the HttpModule hackery and instead relies more on the built-in mechanisms.

If it was up to me I would like to rewrite it using the technique shown in that last link. But currently the Windows authentication is very intermingled with the rest of the login crap and I have seen too many times what happens to Saleslogix when you start pulling on one of the strings!

Updated (2007-03-18): this still works under 7.2.2. Here is the code for the security module helper I created (based on the one distributed by Sage). You will have to adjust the references to SSSWorld assemblies in order to be able to use it, though.

Comments
2 Comments »
Categories
Saleslogix
Comments rss Comments rss
Trackback Trackback

ASP.NET DataBinding Take 2 (or is it 3)

Nicolas Galler | November 20, 2007

Well I finally bit the bullet and implemented the solution I discussed in my previous post on databinding by creating a new data source control. 2 comments:

  1. It was ridiculously easy
  2. It is fucking awesome

So now the code from my datagrid looks like this:

<asp:gridview datasourceid='dsProducts'>
<columns>
<DataBoundField DataField='ProductName'>
</columns>
</asp:gridview>

<sss:SlxEntityDataSource EntitySourceProperty='Products' id='dsProducts' >

The SlxEntityDataSource retrieves the entity bound to the page and resolves the Products property. That’s pretty much it really… There is an EntityDataSourceView which contains the actual logic needed to retrieve the entity within the collection… It is also possible to just set this to a custom collection. And of course NHibernate automatically persists all this mess (with the help of Saleslogix to save it into the session until we are ready to commit it) without me having to lift one finger. Next in line is adding a small improvement to the GridView to make it possible to edit more than 1 row at a time because having to click Edit/Update every time is a bit of a drag. There is a good post for this on Matt Dotson’s blog.

Well the last comment I can make for now is what a piece of dogshit ObjectDataSource is. I wish I hadn’t spent so much time trying to make it work despite all. The ASP.NET databinding is actually starting to look pretty good!

Comments
No Comments »
Categories
Programming
Comments rss Comments rss
Trackback Trackback

VS2008 Download Sucks

Nicolas Galler | November 20, 2007

Well I tried the VS2008 download 3 times now and it always shuts down at 50% or more – they use that crappy “Akamai” download manager or whatever it is called. If the connection is interrupted it comes up with a message box that interrupts the downloads and won’t resume it until you click OK. If you wait too long the authentication times out and it gives “Requested file not available” error. Then it cancels the download. It will give you a chance to cancel the cancel but it still does it. What a piece of crap. Guess I will wait until it is available on the regular MSDN.

Comments
No Comments »
Categories
Rant
Comments rss Comments rss
Trackback Trackback

Windows Got Grep!

Nicolas Galler | November 19, 2007

Well, almost.

May be old news to a lot of people, but I found out the FINDSTR utility which is installed standard on Win XP machines (probably Win 2k3 as well) has enough options to be a decent replacement to grep in the common cases. Even makes it easier for some things – for example use /S for a recursive search instead of having to build it with find.

For myself I still prefer using grep/find instead (on cygwin)… but will be useful when working on other boxes.

Comments
1 Comment »
Categories
Tricks
Comments rss Comments rss
Trackback Trackback

One to one entities in Slx 72

Nicolas Galler | October 26, 2007

There isn’t any walk-through or “best practice” for displaying one-to-one entities in the Saleslogix 7.2 web forms so I thought I should share my experience.

One to one entities were until recently the best practice for adding custom fields to Saleslogix entities (eg Account). They are still a recommended practice in some cases. Here is the walk through on how to define a 1-to-1 entity named CabAccountMisc, linked to Account. The linked table name is CAB_ACCOUNT_MISC. The primary key is, of course, ACCOUNTID.

  • Create the new CabAccountMisc entity using the wizard.
  • Edit the entity and mark it as “Extension”. Select Account as the extended table (this will make Saleslogix generate the foreign key generator strategy in the HBM file, and automatically generate an Account property on the entity as well as a CabAccountMisc property on the Account).

Now you can add a control to the account form and bind it to CabAccountMisc.FooProperty. Very nice, and it actually all works “out of the box” – too bad there isn’t a FAQ on it! The key was to understand that SLX does not consider 1 to 1 relationships as “relationships” but instead as “extensions”. Weird, but it works, so I’ll take it!

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

« Previous Entries

Categories

  • Experiments (4)
  • Interesting (1)
  • MSCRM (1)
  • Programming (60)
  • Rant (3)
  • Saleslogix (34)
  • Tricks (8)
  • Uncategorized (24)

Post History

  • 2010
    • January (3)
    • March (1)
  • 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