Thursday, 24 December 2009

A Popup Insert control for Dynamic Data (UPDATED)

Well the Popup Control can be used for used for any Web Forms application but for this sample we will use it to give ForeignKey_Edit FieldTemplates a popup Insert page. This will allow you to insert a new item in to the ForeignKeyColumns ParentTable and then set the ForeignKey DropdownList to the new item.

First of all since we are going to be creating a couple of Server Controls may I make a recommendation for Nikhil Kothari and Vandana Datye’s book:

Developing Microsoft ASP.NET Server Controls and Components

Developing Microsoft ASP.NET Server Controls and Components

I found the book very thorough and detailed.

The Popup Controls

I found several articles that offered a popup solution with a return value(s) but decided that none of then offered the solution I wanted for using with Dynamic Data. This is what I wanted:

  • A simple control to drop onto the page that would show a button.
  • Send a return value back to the page that created the popup.
  • and then act on the value returned.

Seems simple enough doesn't it but as always if someone else has done it they want money for it Surprised so I thought I’ll build one and here we go.

Note: I don’t plan to say what is already said in Nikhil and Vandana’s book so this won’t be a Server Controls Tutorial just an explanation of what these controls do (after all the book does it very well).

First of all lets explain what we want to build:

  • PopupButton control
  • PopupClient control
  • JavaScript to do the popup
function NAC_OpenPopup(popupUrl, returnParameter, returnControl, title, width, height) {
    ///<summary>Open a new window in the centre of the screen using the passed in URL.</summary>
    ///<field name="popupUrl" type="string">The popup window's URL as a string.</field>
    ///<field name="returnParameter" type="string">The QueryString Field.</field>
    ///<field name="returnControl" type="string">The name of the control to pass in the url.</field>
    ///<field name="title" type="string">The name of the window as a string.</field>
    ///<field name="width" type="Number" integer="true">The width of the the window as an int.</field>
    ///<field name="height" type="Number" integer="true">The height of the the window as an int.</field>
    
    // pass the id of the return control
    popupUrl = popupUrl + "?" + returnParameter + "=" + returnControl;
    
    // calculate the x and y positions to center the popup.
    var x = 0;
    var y = 0;
    x = (screen.availWidth - 12 - width) / 2;
    y = (screen.availHeight - 48 - height) / 2;

    // setup the features
    var features =
        "'screenX=" + x +
        ",screenY=" + y +
        ",width=" + width +
        ",height=" + height +
        ",top=" + y +
        ",left=" + x +
        ",status=no" +
        ",resizable=no" +
        ",scrollbars=yes" +
        ",toolbar=no" +
        ",location=no" +
        ",modal=yes" + 
        ",menubar=no'";
    
    // open the new window
    var NewWindow = window.open(popupUrl, title, features);
    // set focus to the new window
    NewWindow.focus();
    // return the new window so we can detect the close
    return NewWindow;
}

Listing 1 – NAC_OpenPopup JavaScript function

function NAC_PopupButton1func() {
    // get new popup window
    var NAC_PopupButton1Var = NAC_OpenPopup(
        'Page.aspx',
        'ReturnValue',
        'PopupButton1',
        'Popup',
        '600',
        '600');
    // wait till windows closes
    while (!NAC_PopupButton1Var.closed) { }
    // do postback
    __doPostBack('PopupButton1', '');
}

Listing 2 – This JavaScript function is generated on the page for each PopupButton

// get the return value saved in client
var value = document.getElementById("PopupClient1").value;
// if the value is not empty return it to calling page
if (value != "") {
    // pass value to returnControl
    window.opener.document.getElementById("PopupButton1").value = value
    // call postback on parent window
    window.opener.__doPostBack('PopupButton1', '');
    // close the window
    window.close();
}

Listing 3 – This JavaScript function is generated on the page for each PopupClient

So this is how the these scripts work together; we call the NAC_OpenPopup function from the PopupButton control when the button is clicked and then wait for the popped up window to close and then do a postback to the OnTextChanged event handler. Meanwhile in the client window we have hooked code on the form changed to save the value to the calling window’s PopupButton and then close the window.

using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI;

namespace NotAClue.Web.UI.WebControls
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [ParseChildren(true)]
    [PersistChildren(false)]
    [DefaultEvent("TextChanged")]
    [ToolboxData("<{0}:PopupButton runat=\"server\"></{0}:PopupButton>")]
    public class PopupButton : System.Web.UI.WebControls.Button, IPostBackDataHandler
    {
        private String linkButtonId;

        private static readonly object EventTextChanged = new object();

        /// <summary>
        /// Occurs when [text changed].
        /// </summary>
        [Category("Action")]
        [Description("Raised when text changes")]
        public event EventHandler TextChanged
        {
            add { Events.AddHandler(EventTextChanged, value); }
            remove { Events.RemoveHandler(EventTextChanged, value); }
        }

        /// <summary>
        /// Gets or sets the width of the window.
        /// </summary>
        /// <value>The width of the window.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [DefaultValue(300)]
        public int WindowWidth { get; set; }

        /// <summary>
        /// Gets or sets the height of the window.
        /// </summary>
        /// <value>The height of the window.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [DefaultValue(300)]
        public int WindowHeight { get; set; }

        /// <summary>
        /// Gets or sets the popup window's title.
        /// </summary>
        /// <value>The popup window title.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        public String Title { get; set; }

        /// <summary>
        /// Gets or sets the values.
        /// </summary>
        /// <value>The values.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("The values passed back from the popup.")]
        public String ReturnValues { get; set; }

        /// <summary>
        /// Gets or sets the query string field.
        /// </summary>
        /// <value>The query string field.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the QueryString Field string")]
        [DefaultValue("ReturnValue")]
        public String QueryStringField { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="PopupButton"/> class.
        /// </summary>
        public PopupButton()
        {
            QueryStringField = "ReturnValue";
            WindowHeight = 300;
            WindowWidth = 300;
        }

        protected override void OnInit(EventArgs e)
        {
            linkButtonId = this.UniqueID + "_Button";

            // Define the name and type of the client scripts on the page.
            string scriptName = "NotAClue.Web.UI.WebControls.ResourceFiles.NAC_OpenPopup.js";
            Type scriptType = this.GetType();

            // Get a ClientScriptManager reference from the Page class.
            ClientScriptManager csm = Page.ClientScript;

            // Check to see if the Client Script Include is already registered.
            if (!csm.IsClientScriptIncludeRegistered(scriptType, scriptName))
            {
                // include main Flash Content script
                string urlJS = csm.GetWebResourceUrl(scriptType, scriptName);
                csm.RegisterClientScriptInclude(scriptType, scriptName, urlJS);
            }

            base.OnInit(e);
        }

        // override RenderBeginTag so no tag is output
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
            //base.RenderBeginTag(writer);
        }

        /// <summary>
        /// Renders the control to the specified HTML writer.
        /// </summary>
        /// <param name="writer">
        /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
        /// object that receives the control content.
        /// </param>
        protected override void Render(HtmlTextWriter writer)
        {
            //base.Render(writer);
            //declare the function and variable names
            var popupId = this.UniqueID.Replace("$", "");
            var functionName = "NAC_" + popupId + "Func";
            var variableName = "NAC_" + popupId + "Var";

            StringBuilder script = new StringBuilder();
            script.Append("\n<script language=\"JavaScript\" type=\"text/javascript\">\n");
            script.Append("//<![CDATA[\n");
            script.Append("function " + functionName + "() {\n");
            script.Append("var " + variableName + " = NAC_OpenPopup('" +
                PostBackUrl + "', '" +
                QueryStringField + "', '" +
                this.UniqueID + "', '" +
                Title.Replace(" ", "") + "', '" +
                WindowWidth + "', '" +
                WindowHeight + "');\n");
            //script.Append("while (!" + variableName + ".closed) { }\n");
            //script.Append(Page.ClientScript.GetPostBackEventReference(this, "") + ";\n");
            script.Append("//]]>\n");
            script.Append("}\n");
            script.Append("\n</script>\n");

            writer.Write(script.ToString());

            // render button
            writer.AddAttribute(HtmlTextWriterAttribute.Id, linkButtonId);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, linkButtonId);

            // set CSS styling
            if (!String.IsNullOrEmpty(CssClass))
                writer.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);

            writer.AddAttribute(HtmlTextWriterAttribute.Onclick, "javascript:" + functionName + "();");
            writer.RenderBeginTag(HtmlTextWriterTag.Button);
            writer.Write(Text);
            writer.RenderEndTag();

            // hidden field
            //<input type="hidden" name="HiddenField1" id="HiddenField1" />
            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag();
        }

        // override RenderEndTag so no tag is output
        public override void RenderEndTag(HtmlTextWriter writer)
        {
            //base.RenderEndTag(writer);
        }

        #region IPostBackDataHandler Members
        /// <summary>
        /// When implemented by a class, processes 
        /// postback data for an ASP.NET server control.
        /// </summary>
        /// <param name="postDataKey">
        /// The key identifier for the control.
        /// </param>
        /// <param name="postCollection">
        /// The collection of all incoming name values.
        /// </param>
        /// <returns>
        /// true if the server control's state 
        /// changes as a result of the postback; 
        /// otherwise, false.
        /// </returns>
        public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
        {
            // get the return value into the property
            ReturnValues = postCollection[this.UniqueID];
            return true;
        }

        /// <summary>
        /// When implemented by a class, signals the 
        /// server control to notify the ASP.NET application
        /// that the state of the control has changed.
        /// </summary>
        public void RaisePostDataChangedEvent()
        {
            // raise the OnTextChanged event
            OnTextChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Raises the <see cref="E:TextChanged"/> event.
        /// </summary>
        /// <param name="e">
        /// The <see cref="System.EventArgs"/> 
        /// instance containing the event data.
        /// </param>
        protected virtual void OnTextChanged(EventArgs e)
        {
            EventHandler textChangedHandler = (EventHandler)Events[EventTextChanged];
            // raise the event if a handler is attached.
            if (textChangedHandler != null)
                textChangedHandler(this, e);
        }
        #endregion
    }
}

Listing 4 – PopupButton class

using System;
using System.ComponentModel;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace NotAClue.Web.UI.WebControls
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [Designer("NotAClue.Web.UI.WebControls.PopupClientDesigner, NotAClue.Web.UI.WebControls")]
    [ToolboxData("<{0}:PopupClient runat=\"server\"></{0}:PopupClient>")]
    public class PopupClient : WebControl
    {
        /// <summary>
        /// holds the name of the control 
        /// that we retrun a value to
        /// </summary>
        private String returnValuesControl;

        /// <summary>
        /// Gets or sets the values.
        /// </summary>
        /// <value>The values.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the return values")]
        public String ReturnValues { get; set; }

        /// <summary>
        /// Gets or sets the query string field.
        /// </summary>
        /// <value>The query string field.</value>
        [Browsable(true)]
        [Bindable(true)]
        [Localizable(false)]
        [Category("Appearance")]
        [Description("Gets or sets the QueryString Field string")]
        [DefaultValue("ReturnValue")]
        public String QueryStringField { get; set; }

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="PopupClient"/> class.
        /// </summary>
        public PopupClient()
        {
            QueryStringField = "ReturnValue";
        }

        protected override void OnInit(EventArgs e)
        {
            // get return values control
            try
            {
                returnValuesControl = Page.Request.QueryString[QueryStringField];
            }
            catch (Exception)
            {
                returnValuesControl = String.Empty;
            }
            base.OnInit(e);
        }

        // override the RenderBeginTag so no tag is output
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
            //base.RenderBeginTag(writer);
        }

        /// <summary>
        /// Renders the control to the 
        /// specified HTML writer.
        /// </summary>
        /// <param name="writer">
        /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
        /// object that receives the control content.
        /// </param>
        protected override void Render(HtmlTextWriter writer)
        {
            // hidden field
            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            writer.AddAttribute(HtmlTextWriterAttribute.Value, ReturnValues);
            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag();

            StringBuilder script = new StringBuilder();
            script.Append("\n<script language=\"JavaScript\" type=\"text/javascript\">\n");
            script.Append("//<![CDATA[\n");
            script.Append("    var value = document.getElementById(\"" +
                this.UniqueID + "\").value;\n");

            script.Append("    if(value != \"\") {\n");
            script.Append("        window.opener.document.getElementById(\"" + returnValuesControl + "\").value = value\n");
            script.Append("        window.opener.__doPostBack('" + returnValuesControl + "', '');\n");
            script.Append("        window.close();\n");
            script.Append("//]]>\n");
            script.Append("    }\n");
            script.Append("\n</script>\n");

            writer.Write(script.ToString());

            //base.Render(writer);
        }

        // override the RenderEndTag so no tag is output
        public override void RenderEndTag(HtmlTextWriter writer)
        {
            //base.RenderEndTag(writer);
        }
    }
}

Listing 5 – PopupClient

Note: Just explaining the two controls would take an age so get the book mentioned above Open-mouthed

Integrating it into a Dynamic Data site

We will add an attribute to set whether the PopupButton is displayed and what size the popup windows should be.

/// <summary>
/// An attribute used to specify the filtering behaviour for a column.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class PopupInsertAttribute : Attribute
{
    public int WindowWidth { get; set; }
    
    public int WindowHeight { get; set; }

    public PopupInsertAttribute(int width, int height)
    {
        WindowWidth = width;
        WindowHeight = height;
    }
}

Listing 6 – the PopupInsertAttribute

First we will add the PopupButton to the ForeignKey_Edit field template.

protected void Page_Load(object sender, EventArgs e)
{
    if (DropDownList1.Items.Count == 0)
    {
        if (!Column.IsRequired)
            DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

        PopulateListControl(DropDownList1);
    }
    // setup popup
    var popup = MetadataAttributes.GetAttribute<PopupInsertAttribute>();
    if (popup != null)
    {
        PopupButton1.PostBackUrl = ForeignKeyColumn.ParentTable.GetActionPath("PopupInsert");
        PopupButton1.Visible = true;
        PopupButton1.WindowWidth = popup.WindowWidth;
        PopupButton1.WindowHeight = popup.WindowHeight;
        PopupButton1.Title = ForeignKeyColumn.ParentTable.DisplayName;
    }
}

protected void PopupButton1_TextChanged(object sender, EventArgs e)
{
    string foreignkey = PopupButton1.ReturnValues;
    //if no value just return don't do anything
    if (String.IsNullOrEmpty(PopupButton1.ReturnValues))
        return;

    // reset the DDL
    DropDownList1.Items.Clear();
    PopulateListControl(DropDownList1);
    if (!Column.IsRequired)
        DropDownList1.Items.Add(new ListItem("[Not Set]", ""));

    ListItem item = DropDownList1.Items.FindByValue(foreignkey);
    if (item != null)
        DropDownList1.SelectedValue = foreignkey;
}

Listing 7 – Modified Page_Load and the TextChange event handlers for the ForeignKey_Edit field.

Now for the PopupInsert page first make a copy of the standard Insert page (if using a Web Application Project you must change the pages class name also)

public partial class PopupInsert : System.Web.UI.Page
{
    protected MetaTable table;

    protected void Page_Init(object sender, EventArgs e)
    {
        DynamicDataManager1.RegisterControl(DetailsView1);

        // turn partial render off.
        var scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager != null)
            scriptManager.EnablePartialRendering = false;
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        table = DetailsDataSource.GetTable();
        Title = table.DisplayName;
    }

    protected void DetailsView1_ItemCommand(object sender, DetailsViewCommandEventArgs e)
    {
        if (e.CommandName == DataControlCommands.CancelCommandName)
        {
            // Response.Redirect(table.ListActionPath);
            // close popup cleanly making sure the return value is an empty string
            PopupClient1.ReturnValues = "";
            Response.Write("<script language='javascript'> { self.close() }</script>");
        }
    }

    protected void DetailsView1_ItemInserted(object sender, DetailsViewInsertedEventArgs e)
    {
        // don't respond to the DetailsView1_ItemInserted here anymore
        //if (e.Exception == null || e.ExceptionHandled)
        //{
        //    Response.Redirect(table.ListActionPath);
        //}
    }

    protected void DetailsDataSource_Inserted(object sender, LinqDataSourceStatusEventArgs e)
    {
        if (e.Exception == null || e.ExceptionHandled)
        {
            // get the PK and return it to the parent page
            PopupClient1.ReturnValues = table.GetPrimaryKeyString(e.Result);
        }
    }
}

Listing 8 – PopupInsert page.

Sample Metadata

[MetadataType(typeof(ProductMetadata))]
partial class Product
{
    partial class ProductMetadata
    {
        public Object ProductID { get; set; }
        public Object ProductName { get; set; }
        public Object SupplierID { get; set; }
        public Object CategoryID { get; set; }
        public Object QuantityPerUnit { get; set; }
        public Object UnitPrice { get; set; }
        public Object UnitsInStock { get; set; }
        public Object UnitsOnOrder { get; set; }
        public Object ReorderLevel { get; set; }
        public Object Discontinued { get; set; }
        // Entity Set 
        public Object Order_Details { get; set; }
        // Entity Ref 
        [PopupInsert(600, 500)]
        public Object Category { get; set; }
        // Entity Ref 
        public Object Supplier { get; set; }
    }
}

Listing 9 – sample metadata

An Explanation of how it all works

We add the PopupInsertAttribute to the FK column we want to be able to do an insert on, this causes the ForeignKey_Edit to display the New PopupButton which opens the PopupInsert page.

When you click either the Insert or cancel button this will cause the PopupInsert page to close wither returning an empty string or the ID of the new entity.

The TextChanged event is fired on the PopupButton in the ForeignKey_Edit field template where if there is a new value the drop down list is repopulated and the new item is selected.

Download

Full source for the PopupButton and PopupClient server controls is provided in the sample.

23 comments:

Guillaume said...

Nice, i was looking for that last month so i made one myself (in a few hours so it's perhaps not working as well as yours and it requires changes in master files) but if you're interested take a look here, it doesn't require a lot of code: http://stackoverflow.com/questions/1622330/new-button-for-foreign-key-fields-in-dynamic-data/1738693#1738693

Steve said...

Cool Guillaume, I'll take a look when I get some time, busy busy busy :)

Steve

Anonymous said...

I really like what you done with dynamic data. Hapy new year!

Anonymous said...

I've downloaded the sample zip file, but I couldn't test it as it looks like I need the AdventureWorksLT DB which I don't yet have and I'm having only SQLExpress 2005. However, I'd like to know whether this Popup Insert Control would solve my problem detailed in the first post at http://forums.asp.net/t/1453146.aspx

Steve said...

Hi Mark I will make a Northwind based sample available tonight :D

People are always sying using northwind is lame but everyone has it :D

Steve

Steve said...

The Northwind Version is now available :D

Steve

Anonymous said...

I've been waiting months for this to close my current DD Project. Even thought of abandoning it...Thanks a lot! -->Mark

Rajib said...

No need for it to be sealed tho.

Steve said...

You are of course correct it does not need to be sealed :( I think I just copied and pasted as the starting point for the attribute. changed it now thanks

Steve :D

Anonymous said...

Hi Steve, Thanks for your effort. I try your sample but when i use some code like this in global.asa I get a JScript error:

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
Constraints = new RouteValueDictionary(new { action = "PopupInsert" }),
Model = model
});

routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx")
{
Action = PageAction.List,
ViewName = "ListDetails",
Model = model
});

routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx")
{
Action = PageAction.Details,
ViewName = "ListDetails",
Model = model
});

Can you give me some trick?

Thanks
Carmelo from Italy (cmcampione at gmail dot com)

Steve said...

Hi Carmelo, try turning EnablePartialRender off in site.master as that is probably masking the error.

Anonymous said...

Sheve! You are very very ...bravo. Thanks.

Carmelo

Steve said...

Hi Carmelo, thank you :)

Steve

Mark said...

I need a PopUpEdit and a PopUpView control. Steve, I'm still waiting ever so patiently...

Anonymous said...

Thank you. This is awesome.

If the user leaves the page that created the popup, then, for whatever reason, tries to update a value in the pop-up itself, the application crashes.

Do you have any suggestions on what you would do in those cases?

Steve said...

A very good point thank you, I'll have to think about that. My next step will be3 to use the Ajax Popup from the Ajax Controil tookit.

Steve

Stefano said...

Hey Steve,
great job !
Is it possible to change the text (New) showed inside the button?
I've tried but it seems that there is no a public property....
Thanks in advance

Stefano

Steve said...

not in that version unless you change the render code it's very old code.

I am building an open source projects on Codeplex http://ddextensions.codeplex.com I should get it finished and ready for consumpsion onve the Christmas holidays you can get the source from there now but I only have the server side code uploaded at the moment.

Steve

Jorge Javier Gutierrez said...

Dear Steve, could you use the Ajax Popup from the Ajax Controil tookit?

Thanks in advance.

Jorge

Jorge Javier Gutierrez said...

Dear Steve, could you use the Ajax Popup from the Ajax Controil tookit for this solution?

Thanks in advance.

Jorge

Steve said...

Hi Jorge , yes that is a possibility but I have not done that yes as I would be a lot more complex than another custom page template.

Steve

JP said...

if this can help anybody, don't forget that to add edit this line in Global.asax.cs :
Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert|PopupInsert" }),

Stephen Naughton said...

Thanks JP making that change is required :)