Wednesday, March 28, 2012

ContextMenus from PopupExntender without the xml script?

Hello,

A while back I figured out a way to implement "context menus" on top of the built-in Asp:menu control and documented that here:

http://apps.ultravioletconsulting.com/misc/ContextMenus.aspx

This approach uses a hyrbid server/client data-binding technique, but this approach still relied on rendering menu data from the server and relying on the messy HTML table that were output. I was able to drastically reduce the amount of data / javascript passed to the client to that point that it was nothing more than an array embedded in a click handler:

<table style="border:1px solid green"border="0"cellpadding="0"cellspacing="0">

<tr valign="top">
<td>
1: $5 <button value="Do it Now!">Do it Now!</button>
</td>
<td class="MenuCell"
onclick="ContextMenu_Place(event,'Menu1', [1,5])"></td
</tr>
</table>

I tried to use the PopupControl Extender instead to popup a more static block ot HTML / div tags, and it worked fine, except it generates a gigantic amount of XMLScript for each instance. I really need to do nothing more than pass the data values and the location on the screen where the popup should occur. I don't need all the extra Behavior tags, etc.


Any ideas?

Thanks,

Josh

Hi Josh,

The latest release (60914 on 9/15) updated a number of controls to use functionality similar to DynamicPopupate. PopupControl, and others, now have a DynamicContextKey property that may do exactly what you need.

Thanks,
Ted

Thanks Ted,

I ended up writing my own simple ContextMenu_Place function that calls a client-side template/data-binding function to handle this functionality. I am still having one problem though with __doPostBack

Is it possible to manually specify the second parameter in a __doPostBack handler?

I have this code:
<asp:LinkButton runat="server" ID="actTestServerSide"
Text="Test of Server Side Link Button"
CommandName="Action"
CommandArgument="$1"
/>
The rendered code looks like this:

BLOCKEDSCRIPT__doPostBack('ctl00$ctl00$ctlPagePlaceHolder$MyAccountContentPlaceHolder$ctlWonView$actTestServerSide','')

That second parameter is empty though!

Ineed to be able to have code on the _client side_ replace the $1 with avalue of my choice before the actual link gets submitted. The reasonfor this is that I have a GridView with 100 items in it, and I passdown one static block of HTML code as a client-side template for acontext menu that operates on the line item of each GridViewRowinstance. I then have a click handler which does something like:

onclick="ContextMenu_Place(event,'ctlPathGoesHere$blah$blah$blah','ctl00Path$blah$blah',[],['189884','44394743']);
TheContextMenu_Place function copies the template code and then uses thevalues of the second array to replace the placeholders. The firstarray, empty here, specifies the css class name of rows in the menuthat need to be grayed out.

The static block looks like this when rendered to the client side:

 <table id="blah_blah_Container"border="0"cellpadding="0"cellspacing="0"class="ActionsContextMenuContainer">
<tr valign="top">
<td>
<div id="blah$blah">
<div class="actContact"><a title="Contact"href="/Contact.aspx?Item=$1">Contact</a></div>
<div class="actPay"><a title="Pay"href="/Pay.aspx?Item=$1">Pay</a></div>
<div class="actTestServerSide">
<a id="blah$blah$blah"
href="BLOCKED SCRIPT__doPostBack('$blah$blah$blah','')">Test of Server Side Link Button</a>
</div>
</div>
 </td>
</tr>
</table
In the first two cases, the links are to external pages, not POSTBACKS. But in the last case, the action should be POSTBACK, so I need that last line to read:

href="BLOCKED SCRIPT__doPostBack('$blah$blah$blah','$1')">Test of Server Side Link Button</a
This will enable me to use my client side templating function to replace the argument with the actual line item ID.

But, I can't figure out how to get that second parameter of __doPostBack to be non-blank.

Is this feasible? I suppose I could manually replace instances of '') but that seems like a major hack if I can instead get my $1 in there properly.

 
Thanks,
Josh 



Hi Josh,

I'm definitely not an expert on__doPostBack (for that you should probably ask this in thegeneral ASP.NET forums). The only way I know of accessing the optional second event arg is by implementingSystem.Web.UI.IPostBackEventHandler.

Also - Davidblogged about how to get this scenario working withModalPopup and some of the new Toolkit features if you were interested.

Thanks,
Ted

Ted, thank you,

I was able to make a neat little control to accomplish this.


I created one control named ContextMenu inheriting from ASP:Menu.

This allowed me to use <asp:MenuItem> instances instead of hard coding the HTML like I had above.

Next, I overrode the CreateChildControls method to create essentially that same blob of HTML above instead of letting the built in behavior render the HTML.

I added an ASP:Button and an ASP:HiddenField and wrapped them in a DIV with display:none.

Added a Command event to the ContextMenu class of type CommandEventHandler.

Next I made it a convention that if the Target property of the menuItem was set to "POSTBACK", then I would use the Page.ClientScript.GetEventReference(object, argument, registerForEventValidation) method. This actually creates the call to __doPostBack. So, I specified my hidden ASP:Button as the target. In the Value property of the asp:MenuItem, I made it so that "actSomeCommand|$0" would mean that actSomeCommand would be the CommandName, while $0 would be passed down to the client side to allow for the client-side data-binding.

In the href for the anchor tag, I prefixed the call to __doPostBack with a call to document.getElementById('id_of_the_hidden_field_here').value = '$0'. So this allowed my line-item data to be populated just in time for the pop up.

Within a private Command handler for the actual Asp:Button, I read the value of the hidden field -- because I still could not get the real argument to get passed-- then created my own CommandEventArsg and fired that event up to the calling client code.

The end result lets me do this:

<auc:ContextMenu ID="ctlContextMenu" runat="server">
<Items>
<asp:MenuItem
Value="actBuyNow"
ToolTip="Buy this item now!"
Text="Buy Now!"
NavigateUrl="~/Site/ViewItem.aspx?Item=$0#BuyItem"
Target="__self"
/>
<asp:MenuItem
Value="actBid"
ToolTip="Bid on this item"
Text="Bid"
NavigateUrl="~/Site/ViewItem.aspx?Item=$0#PlaceBid"
Target="__self"
/>
</Items>
</auc:ContextMenu
And in my GridView:

<asp:TemplateField HeaderText="Actions" HeaderStyle-CssClass="TextCentered" ItemStyle-CssClass="GridItemActions">
<ItemTemplate>
<auc:ContextMenuPopup
runat="server"
ContentControlContainerID="<%# ctlContextMenu.ClientID %>"
ContentControlID="<%# ctlContextMenu.ContentControlID %>"
id="ctlContextMenuPopup"
/>
</ItemTemplate>
</asp:TemplateField>

-- Note, I'm not so sure I really need both a ContentControlContainerID and ContentControlID on second thought. I could have just had a private convention with my javascript for fetching the actual menu item Divs out of the container, but this works...

Finally, in my code behind:

First, wire up the event handler on the ContextMenu:

ctlContextMenu.Command +=
new CommandEventHandler(ctlContextMenu_Command);

Later, during data binding of my GridView:

ContextMenuPopup menu = e.Row.FindControl("ctlContextMenuPopup")
as ContextMenuPopup;
if (null != menu)
{
menu.DataBind();
}

if (null != menu)
{
string[] lineItemData = new string[2];
lineItemData[0] = item.ItemID.ToString();
lineItemData[1] = item.SellerUserID.ToString();

List<string> hiddenItemIds = new List<string>();

if (0 == item.IsActive())
{
hiddenItemIds.AddRange(new string[] { "actBuyNow", "actBid" });
}

menu.CompleteContextMenu(hiddenItemIds.ToArray(), lineItemData);
}

This call to CompleteContextMenu generates the necessary javascript handler to be added to the container level. It also specifies, depending on the state of the item, which items of the menu should be disabled upon popup.

Last but not least,

protected void ctlContextMenu_Command(object sender, CommandEventArgs e)
{
string action = e.CommandName;
string data = e.CommandArgument as string;

AddFeedback(string.Format("Received command: {0} with argument {1}", action, data));
}

On the client side:

<div class="ContextMenuContainer">
<span id="ctl00_ctl00_ctlPagePlaceHolder_ctlMyView_ctlItems_ctl02_ctlContextMenuPopup_ctlActionsContextMenuLink"
onclick="ContextMenu_Place(event,'ctl00_ctl00_ctlPagePlaceHolder_ctlMyView_ctlContextMenu',
'ctl00_ctl00_ctlPagePlaceHolder_ctlMyView_ctlContextMenu_ctlContextMenuContent',
[],
['189884','44394743'],
this,
);">
Select <img src="../../Controls/ContextMenu_Resources/dwn.gif"align="absmiddle"style="height:27px;width:30px;border-width:0px;"/>
</span>
</div>

Looking back, it was quite a challenge to get this thing working! But, it was fun to write and worth it...

Josh

No comments:

Post a Comment