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

Cascading Picklist inside a ListView – A few more findings on ASP.NET DataBinding

Nicolas Galler | May 28, 2008

Cascading picklists (or dependent dropdowns, however you want to call them) are a pretty common occurrence and there is a common pattern in ASP.NET to address it, it goes like this:

<asp:DropDownList runat="server" ID="cboType"
     AppendDataBoundItems="true" AutoPostBack="true" DataSourceID="dsTypes">
  <asp:ListItem Text="" Value="" />
</asp:DropDownList>
<asp:DropDownList runat="server" ID="cboSubType"
     AppendDataBoundItems="true" AutoPostBack="true" DataSourceID="dsSubTypes">
  <asp:ListItem Text="" Value="" />
</asp:DropDownList>
<asp:ObjectDataSource runat="server" ID="dsSubTypes" TypeName="CustomerDao" SelectMethod="GetSubTypes">
<SelectParameters>
  <asp:ControlParameter ControlID="cboType" Name="type" Type="String" PropertyName="SelectedValue" />
</SelectParameters>
</asp:ObjectDataSource>
<asp:ObjectDataSource runat="server" ID="dsTypes" TypeName="CustomerDao" SelectMethod="GetTypes"/>

What happens if you want to have the dropdown displayed in an editable list of data though, like so:

image

You wish you could write something like this:

<asp:DropDownList OnDataBound="cboSubType_DataBound" runat="server" ID="cboSubType"
  SelectedValue='<%# Bind("SubTypes") %>'
  AppendDataBoundItems="false" DataSourceID="dsSubTypes">
    <asp:ListItem Text="" Value="" />
</asp:DropDownList>

But in reality this will give 2 kinds of error:

  • ‘cboSubType’ has a SelectedValue which is invalid because it does not exist in the list of items
  • Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control

An additional problem is that if you disable the ViewState for the page (as I tend to routinely do) the "SelectedValue" property of the DropDownList is not correctly updated anymore.

In order to tackle these problems I took inspiration in the code listed at http://webswapp.com/codesamples/viewsource.aspx?file=~/codesamples/aspnet20/dependentlists/datalist.aspx (there are a few other neat tricks on the page so I recommend the read!).

The solution I came up with boils down to 2 elements:

  • In the DataBound event of the DropDownList, retrieve the value (either from the postback data, or from the current data) and select it.
DropDownList cboSubType = (DropDownList)sender;
ListViewDataItem parentItem = (ListViewDataItem)cboSubType.NamingContainer;
cboSubType.ClearSelection();
String prevValue = null;
if (IsPostBack)
{
    prevValue = Request.Form[cboSubType.UniqueID];
}
else
{
    if (parentItem.DataItem != null)
    {
        prevValue = ((Customer)parentItem.DataItem).SubType;
    }
}
ListItem li = cboSubType.Items.FindByValue(prevValue);
if (li != null)
    li.Selected = true; 
  • In the ItemUpdating event of the listview I explicitely retrieve the value (from the postback because the SelectedValue may not be valid at that point) and set it in the updated values:
DropDownList cboSubType = (DropDownList)lstCustomers.Items[e.ItemIndex].FindControl("cboSubType");
e.NewValues["SubType"] = Request.Form[cboSubType.UniqueID];

This is not quite as bad as the solution I started with (which involved checking something like Request.Form[lstContacts.UniqueId + "$ctrl00$cboSubType"]) but I still think I must be missing something obvious.  Oh well, maybe the light will come on at some point, until then I got my stuff working.

The code is here in case it is of use to anyone (or in case I need a refresher in 2 months, hah!).

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

Prevent Caching of Script Service AJAX calls – the lazy way

Nicolas Galler | February 20, 2008

“REST” style AJAX calls can be cached by the browser if the server is not properly configured. For some services this is a good thing (think, a mapping service), but often it is a hassle.

The way I see it there are 3 ways to get around it:

  • The “proper” way is to configure the caching on the server app. For example in ASP.NET use Response.Cache.SetCacheability(HttpCacheability.NoCache), or use the <%@ OutputCache %> tag.
  • The “fix it from the client” way is to add a random parameter to the Javascript. This way the query is different every time. If the server can accept POST as well as GET you can also use a POST since those are not cached.
  • The “lazy” way is to force a header from the server configuration. In IIS this is super easy:
    1. edit the properties of the directory that contains your script services
    2. go to HTTP Headers
    3. Add a custom header: name is “Cache-Control” and value is “no-cache”
Comments
No Comments »
Categories
Programming
Tags
AJAX, ASP.NET
Comments rss Comments rss
Trackback Trackback

One more thought on ASP.NET Aynchronous Handlers

Nicolas Galler | February 7, 2008

OK, this is going to be really obtuse to anyone who hasn’t played with IHttpAsyncHandler, but might save me a headache when I try debugging the same problem in 2 months.

Initially I had thought that an asynchronous handler would execute “out of context”… that is, the process would be:

  1. Create context, begin request
  2. Invoke “BeginProcessRequest” from the handler
  3. End request, destroy context (INCORRECT!)
  4. Later on, when the handler finishes – return the result, but do not call End request again, since we are not on the same thread anymore

Looking back, this does not make much sense – funny how things tend to look SOOO obvious once you figured them out. But for some odd reason I thought the HttpContext would not survive accross different thread and so everything
had to happen on the initial thread.
Here is what really happens:

  1. Create context, begin request
  2. Invoke “BeginProcessRequest” from the handler
  3. Go on and do stuff while the handler works
  4. Once the handler finishes, call End request, and return the result – this could happen on a different thread than the one we started on!

One more note of interest – in the “BeginRequest” event, HttpContext will be available, but context.Handler is still null. Context.Request.Path is available, though.

Comments
No Comments »
Categories
Programming
Tags
ASP.NET
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

Enabling log4net logging in SLX 72

Nicolas Galler | January 20, 2008

The Saleslogix web client comes packaged with a wonderful logging system called log4net. This enables us to receive very detailed information about what is going on in every request, how the data is being read from the database, what business rules or events are being executed, etc. Unfortunately it is all disabled in the default installation. While I hope this changes in the near future to a configuration option, here is how you can turn it on for now. Warning: there is a significant performance impact to logging the debugging information – make sure you tweak this before deploying on production (you can restrict it to log only warning or errors).

  • Edit the web.config file, you need to add a line in the top (after the <configSections> tag) that reads:

    <section name="log4net"
      type="log4net.Config.Log4NetConfigurationSectionHandler,log4net,
        Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821" />
  • At the end of your web.config file (right before the closing </configuration> tag) you will need to paste the log4net configuration proper. I won’t lie, this is a pretty decent chunk of XML and I usually just carry the same one around. But in truth it is pretty simple: we are defining Appenders which is where you want to send the log output (eg to files, or to the event viewers), Loggers which are settings for where the log comes from (for example you can specify that you only want log messages of a certain severity for a given logger, or decide to send messages from some loggers to emails and not others), and finally a root setting which is simply the default settings for all loggers. These are the settings that I use for development – they configure the logger to show only warnings from NHibernate (you can change that to DEBUG but be warned that there is a lot of info and it does have a significant effect on performance at that level), and all messages from any other part of the system. Everything is sent to a text file under the Log subdirectory:
    <!-- This section contains the log4net configuration settings -->
    <log4net debug="false">
      <appender name="rollingFile" type="log4net.Appender.RollingFileAppender" >
    
        <param name="File" value="Log/log.txt" />
        <param name="AppendToFile" value="false" />
        <param name="RollingStyle" value="Date" />
        <param name="DatePattern" value="yyyy.MM.dd" />
        <param name="StaticLogFileName" value="true" />
    
        <layout type="log4net.Layout.PatternLayout">
          <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
        </layout>
      </appender>
    
      <!-- Setup the root category, add the appenders and set the
         default priority -->
    
      <root>
        <priority value="DEBUG" />
        <appender-ref ref="rollingFile"/>
      </root>
    
      <logger name="NHibernate">
        <level value="WARN" />
      </logger>
    
      <logger name="NHibernate.SQL">
        <level value="ALL" />
      </logger>
    </log4net>
  • Create a Log directory under the folder and set the permission to be modifiable by Everyone – this is where log4net will send the output.
  • You are almost done, the last thing is you need to create a file called Global.asax in the root of the web client with the following code to initialize the logging system:

    <%@ Application Language="C#" %>
    
    <script runat="server">
    
        private static log4net.ILog _log;
    
        /// <summary>
        /// Initialize web leads library.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>   
        void Application_Start(object sender, EventArgs e)
        {
            log4net.Config.XmlConfigurator.Configure();
            _log = log4net.LogManager.GetLogger(typeof(global_asax));
            _log.Info("Application Started");
        }
    
        void Application_End(object sender, EventArgs e)
        {
            //  Code that runs on application shutdown
            _log.Info("Application Ended");
            log4net.LogManager.Shutdown();
        }
    
        /// <summary>
        /// Redirect to generic error page.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>    
        void Application_Error(object sender, EventArgs e)
        {
        }
    
        void Session_Start(object sender, EventArgs e)
        {
            // Code that runs when a new session is started
    
        }
    
        void Session_End(object sender, EventArgs e)
        {
            // Code that runs when a session ends. 
            // Note: The Session_End event is raised only when the sessionstate mode
            // is set to InProc in the Web.config file. If session mode is set to StateServer 
            // or SQLServer, the event is not raised.
    
        }
    
    </script>

That’s it! Browse to the client and make sure the “log.txt” file gets created.

There is one more thing I like to do, add this code in the Page_Load of SmartParts/General/GeneralException.ascx, it will give you a traceback in the log whenever you hit an exception (the ones that are reported with the “Saleslogix has encountered an error” screen, not the ones that give you a Yellow Page Of Death):

if (Page.Request.QueryString["exceptionid"] != null)
{
  Exception ex = Sage.Platform.Application.ApplicationContext.Current.State[
     Page.Request.QueryString["exceptionid"]] as Exception;
  if (ex != null)
  {
    ExceptionID.Text = ex.Message;
    log4net.LogManager.GetLogger(typeof(GeneralException)).Warn("Exception Reported", ex);
  }
}

Now in your code, if you want to output some log messages, put code similar to this at the top of your class to declare a logger:

private static readonly log4net.ILog LOG =
  log4net.LogManager.GetLogger(typeof(myclass));

and in your code add calls to LOG.Info, LOG.Debug, etc.

See the log4net home page for more information about this excellent tool.

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

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

Categories

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

Post History

  • 2010
    • January (3)
    • March (3)
    • April (2)
    • August (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