RaisePostBackEvent called on wrong control - asp.net

I have spent two days with another colleague investigating this. I was surprised as most solutions discussing this problem either have the wrong solution or a solution that works, I think, for the wrong reasons.
We have a custom button control that needs to raise a ServerClick event when it is pressed. Here is the summarised code:
public class MyButton : WebControl, IPostBackEventHandler
{
protected HtmlGenericControl _Button;
protected string _OnClick = "";
protected string _Name;
public event EventHandler ServerClick;
// etc...
public MyButton()
{
Width = Unit.Pixel(100);
_Button = new HtmlGenericControl("button");
Controls.Add(_Button);
}
protected override void Render(HtmlTextWriter writer)
{
_Button.Attributes.Add("id", string.IsNullOrEmpty(_Name) ? base.ID : _Name);
_Button.Attributes.Add("name", _Name);
// etc...
_OnClick = Page.ClientScript.GetPostBackEventReference(this, "");
_Button.Attributes.Add("onClick", _OnClick);
// etc...
ID = String.Empty;
Name = String.Empty;
AccessKey = String.Empty;
TabIndex = -1;
Width = Unit.Empty;
base.Render(writer);
}
protected virtual void OnServerClick()
{
if (this.ServerClick != null)
{
this.ServerClick(this, new EventArgs());
}
}
public void RaisePostBackEvent(string eventArgument)
{
this.OnServerClick();
}
}
On the browser end the code uses two of these buttons
<form>
<!-- etc ... -->
<div class="row actionBar">
<PGSC:MyButton Name="btnAccept" ID="btnAccept" LabelID="3244" TabIndex="70" runat="server" OnServerClick="AcceptClickHandler"/>
<PGSC:MyButton Name="btnClose" ID="btnClose" LabelID="349" OnClick="window.returnValue=frmMMD.hdnMmdId.value;window.close();" TabIndex="80" runat="server" />
</div>
</form>
The Problem:
The event is not raised on the accept button. Debugging reveals that RaisePostBackEvent is called but on the Close button, which does not have a ServerClick handler attached, hence nothing happens. No event handlers get called.
Notes:
The problem is not seen if there is only one MyButton on the page.
If the buttons are reordered such that the accept button is the last on the page, it starts working.
Moving the buttons outside of the form tag causes events to work as expected, and the accept buttons event handler is called correctly.
Implementing IPostBackDataHandler and calling RaisePostBackEvent() from IPostBackDataHandler::RaisePostDataChangedEvent() causes the event to be raised correctly on the accept button when inside the form tag.
Calling RegisterRequiresRaiseEvent(btnAccept) during PageLoad routes events correctly to the accept button.
The Question:
What is the correct solution from the ones that work above? Or is there another solution? We need it to work such that multiple buttons on the page can raise independent click events, without regard to their order or position on the page.
My Thoughts:
This problem seems to be discussed here: http://forums.asp.net/t/1074998.aspx?ASP+NET+RaisePostbackEvent+Issues
One is lead to believe that calling __doPostback() with the correct __EVENTTARGET should automatically route the event correctly to the button, but this is not happening in reality. It only happens if we also implement IPostBackDataHandler. Many solutions on the web seem to point to __doPostback, UniqueID etc as the culprit when actually implementing IPostBackDataHandler is what seemingly fixes the issue.
The Control implements IPostBackEventHandler but not IPostBackDataHandler. I think this is correct because the control does not need to raise any data driven events. So implementing IPostBackDataHandler to get it working seems like a hack.
Using RegisterRequiresRaiseEvent is unintuitive and besides will not work if multiple buttons on the page would like to raise events.
I wonder, how does an asp:Button do it?

I've simulated a situation.
Hope it helps.
There is the MyButton WebServerControl class:
[DefaultProperty("Text")]
[ToolboxData("<{0}:MyButton runat=server></{0}:MyButton>")]
public class MyButton : WebControl, IPostBackEventHandler
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["Text"] = value;
}
}
protected HtmlGenericControl _Button;
protected string _OnClick = "";
protected string _Name;
public event EventHandler ServerClick;
// etc...
public MyButton()
{
Width = Unit.Pixel(100);
_Button = new HtmlGenericControl("button");
Controls.Add(_Button);
}
protected override void Render(HtmlTextWriter writer)
{
_Button.Attributes.Add("id", string.IsNullOrEmpty(_Name) ? base.ID : _Name);
_Button.Attributes.Add("name", _Name);
// etc...
_OnClick = Page.ClientScript.GetPostBackEventReference(this, "");
_Button.Attributes.Add("onClick", _OnClick);
// etc...
ID = String.Empty;
//Name = String.Empty;
AccessKey = String.Empty;
TabIndex = -1;
Width = Unit.Empty;
base.Render(writer);
}
protected virtual void OnServerClick()
{
if (this.ServerClick != null)
{
this.ServerClick(this, new EventArgs());
}
}
public void RaisePostBackEvent(string eventArgument)
{
this.OnServerClick();
}
}
I then used my web server control in a project, let's say this is the default.aspx:
<div><cc1:MyButton ID="btnAccept" runat="server" TabIndex="70" OnServerClick="AcceptClickHandler" />
<cc1:MyButton ID="btnClose" Text="Close" Width="256px" LabelID="349" runat="server" TabIndex="80" /></div><div>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
</div>
And in default.aspx.cs I've implemented simply the event:
protected void AcceptClickHandler(object sender, EventArgs e)
{
Label1.Text = DateTime.Now.ToLongTimeString();
}
The AcceptClickHandler fires only when clicking on the Accept button and not
on the Close button.
Sorry if I didnt get the problem right.

Related

Setting parent page controls visibility from Child user control page

I have a parent page Page1 which has button1. Page1 has a usercontrol uc1. uc1 has an update panel inside which a grid grid1 is present. I am trying to set Page1.button1's visibility to false, depending on the row command event(there are some if conditions in the row command event) of uc1.grid1. I am setting Page1.button1's visibility in the following way:
Create a IsButton1Visible property in uc1. Set the property in UC1.Grid1.RowCommand to false, on page1 PreRender event, access IsButton1Visible and set Page1.button1 visibility.
Even though in quick watch Page1.button1 visibility is set to false at the line of assignment, when I see the UI, it is still visible. I don't know what I am doing wrong. Or the way that I am getting hold of button1 and its visibility is not correct.
In general can we set a Parent page's control's property from a user control during the user control event?
If you use the event-driven model approach
Delegate/EventArgs code:
public class ButtonVisiblityEventArgs : EventArgs
{
public ButtonVisiblityEventArgs(bool visible)
{
this.Visiblity = visible;
}
public bool Visiblity { get; private set; }
}
public delegate void UpdateParentButtonVisibilityEventHandler(object sender, ButtonVisiblityEventArgs args);
User control code:
public event UpdateParentButtonVisibilityEventHandler RaiseUpdateParentButtonVisibilityEvent;
private void RequestParentButtonVisibilityChange(bool setVisible)
{
if (RaiseUpdateParentButtonVisibilityEvent != null)
{
RaiseUpdateParentButtonVisibilityEvent(this, new ButtonVisiblityEventArgs(setVisible));
}
}
And in your command handler, just call:
RequestParentButtonVisibilityChange(false);
whenever you want to hide the button. On your page:
protected void Page_Load(object sender, EventArgs e)
{
this.RaiseUpdateParentButtonVisibilityEvent += new UpdateParentButtonVisibilityEventHandler(uc_RaiseUpdatecurrentDisplayPanelRequestEvent);
}
private void uc_RaiseUpdatecurrentDisplayPanelRequestEvent(object sender, ButtonVisiblityEventArgs args)
{
button1.Visible = args.Visiblity;
}
If the problem you are having is that your button lives outside of the update panel, you can do the following. Page codebhind:
protected void Page_Load(object sender, EventArgs e)
{
string hideScript = string.Format("function updateButtonVisibility( visibility ) {{ var button = $('#{0}'); if (visibility) {{ button.show(); }} else {{ button.hide(); }} }}", this.button1.ClientID);
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "updateButtonVisibility", hideScript, true);
}
And in your user control command handler:
bool shouldButtonBeVisible = false; //update this appropriately in your logic
ScriptManager.RegisterStartupScript(this, this.GetType(), "upUpdateButtonVisibility", "updateButtonVisibility(" + shouldButtonBeVisible ? "true" : "false" + ");", true);
Please note that this creates a TIGHT dependency between your UC and the page. It requires that any page that consumes this control has registered this script. There are ways to get around this (such as setting a function script callback to call, detecting if that javascript function exists, etc), but this should at least get you moving.
If there is something specific on the page after your update panel finishes that you could key off, it might be better to register an end request handler
$(function() { Sys.WebForms.PageRequestManager.getInstance().add_endRequest(updatePanelEndRequestHandler); } );
function updatePanelEndRequestHandler() {
var shouldBeVisible = $('.MyClassThatSaysIShouldntAllowMoreButtons').length > 0; //do some checking on the grid
updateButtonVisibility(shouldBeVisible);
}
you can put your user controls inside panels on your parent pages and change the visibility.
e.g.
<asp:Panel runat="server" ID="pnlQuote">
...
</asp:Panel>
<asp:Panel runat="server" ID="pnlContact">
<uc1:ContactForm runat="server" ID="ContactForm " />
</asp:Panel>
From the child control you can make a button click event which does something like this
protected void btnBackToQuote_Click(object sender, EventArgs e)
{
Panel pnlQuote = this.Parent.FindControl("pnlQuote") as Panel;
Panel pnlContact = this.Parent.FindControl("pnlContact") as Panel;
pnlQuote .Visible = true;
pnlContact.Visible = false;
}

Create a class that inherits from textbox and can display calender on click

I am trying to build a calendar that pop up when you click on textbox (in asp.net);
For this task I build a new class that inherit from text box, and inside the new class constructor I put the calendar instance, but the thing is, when I inctance my new class I only get the textbox control and on click I do not get the calender.
Does anyone have an idea way or how can i fix this? my code is:
public class TextBoxCalendar : TextBox
{
public TextBoxCalendar( int id)
{
this.ID = id.toString();
initCalender();
}
private void initCalender()
{
CalendarExtender ce = new CalendarExtender();
ce.ID = "calender";
ce.TargetControlID = this.ID;
}
}
Aside from just initializing the control, you need to add it to the controls collection at some point during the rendering process:
protected override void Render(HtmlTextWriter writer)
{
CalendarExtender ce = new CalendarExtender();
ce.ID = "calender";
ce.TargetControlID = this.ID;
Controls.Add(ce);
//render the contents to the screen
RenderContents(writer);
}
You have to override OnInit method of TextBox and write the initCalender code inside it.
protected override void OnInit(EventArgs e) {
CalendarExtender ce = new CalendarExtender();
ce.ID = "calender";
ce.TargetControlID = this.ID;
}
You could use Jquery Datepicker.

ViewState error with dynamically built menu

I have a Menu control that I dynamically populate with categories. When a user clicks a category, the postback should populate a grid with products in that category. However, I seem to have tried every way possible, i.e. whatever page life cycle time, only to always get this error:
Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request.
Here is all my code:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
BuildCategoryMenu();
}
}
protected void categoryMenu_ItemClick(object source, DevExpress.Web.ASPxMenu.MenuItemEventArgs e)
{
var catId = new Guid(e.Item.Name);
ListProductsByCatId(catId);
}
private void BuildCategoryMenu()
{
var cats = _categoryService.ListActive();
categoryMenu.Items.Clear();
foreach (var cat in cats)
{
categoryMenu.Items.Add(new MenuItem { Text = cat.Name, Value = cat.id.ToString() });
}
}
private void ListProductsByCatId(Guid catId)
{
productGrid.DataSource = _productService.ListByCatId(new Guid("a5c2f0ef-a3cc-4af1-abac-37f1be6a5c74"));
productGrid.DataBind();
}
Here is my Menu:
<asp:Menu ID="categoryMenu" runat="server" EnableViewState="false">
</asp:Menu>
EnableViewState is only false because it didn't work with true either.
One answer to this is to avoid the postbacks and code URL's into the menu items. More RESTful, but probably making AJAXing the menu a bit harder.

Add ability to provide list items to composite control with DropDownLIst

I'm creating a composite control for a DropDownList (that also includes a Label).
The idea being that I can use my control like a dropdown list, but also have it toss a Label onto the page in front of the DDL.
I have this working perfectly for TextBoxes, but am struggling with the DDL because of the Collection (or Datasource) component to populate the DDL.
Basically I want to be able to do something like this:
<ecc:MyDropDownList ID="AnimalType" runat="server" LabelText="this is what will be in the label">
<asp:ListItem Text="dog" Value="dog" />
<asp:ListItem Text="cat" Value="cat" />
</ecc:MyDropDownList>
The problem is, I'm not extending the DropDownList class for my control, so I can't simply work it with that magic. I need some pointers to figure out how I can turn my control (MyDropDownList), which is currently just a System.Web.UI.UserControl, into something that will accept List items within the tag and ideally, I'd like to be able to plug it into a datasource (the same functions that the regular DDL offers).
I tried with no luck just extending the regular DDL, but couldn't get the Label component to fly with it.
After doing some digging and searching I found a solution that works. Hopefully this will help someone else out in the future:
[ParseChildren(true, "Items")]
public class EDropDownList : CompositeControl, IValidatedFields
{
public string PromptingText { get; set; }
public string Value { get; set; }
public Label __Label { get; set; }
private ListItemCollection _items;
public DropDownList __DropDownList;
public ListItemCollection Items
{
get { return _items; }
set
{
if (_items != value)
{
_items = value;
}
}
}
public string Type { get { return "DropDownList"; } }
public EDropDownList()
{
__Label = new Label();
}
protected override void CreateChildControls()
{
__DropDownList = new DropDownList();
foreach (ListItem myItem in _items)
{
__DropDownList.Items.Add(myItem);
}
Controls.AddAt(0, __Label);
Controls.AddAt(1, __DropDownList);
}
protected override void OnLoad(EventArgs e)
{
// label section
__Label.Text = PromptingText+"<br />";
__Label.ForeColor = Color.Red;
__Label.Visible = false;
// ddl section
if (Page.IsPostBack)
Value = __DropDownList.SelectedValue;
}
}
The easiest thing would be to go back to your original option of extending the DropDownList control. What problems did you have getting the label to work with it? Those problems are probably easier to solve?

Possible to have a inner control on a custom server control?

I would like to be able to do something like:
<ui:Tab Title="A nice title">
<TabTemplate>
<asp:Literal runat="server" ID="SetMe">With Text or Something</asp:Literal>
</TabTemplate>
</ui:Tab>
but also be able to do:
<ui:Tab Title="A nice title">
<TabTemplate>
<asp:DataList runat="server" ID="BindMe"></asp:DataList>
</TabTemplate>
</ui:Tab>
Answer code I eventually came up with:
[ParseChildren(true)]
public class Node : SiteMapNodeBaseControl, INamingContainer
{
private ITemplate tabTemplate;
[Browsable(false),
DefaultValue(null),
Description("The tab template."),
PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(typeof(TabTemplate))]
public ITemplate TabTemplate
{
get { return tabTemplate; }
set { tabTemplate = value; }
}
protected override void CreateChildControls()
{
if (TabTemplate != null)
{
Controls.Clear();
TabTemplate i = new TabTemplate();
TabTemplate.InstantiateIn(i);
Controls.Add(i);
}
}
protected override void Render(HtmlTextWriter writer)
{
EnsureChildControls();
base.Render(writer);
}
}
public class TabTemplate : Control, INamingContainer
{
}
The ParseChildren attribute tells .NET whether to treat your control's children as properties or as controls. For your first example, you want to treat children as controls, so add
[ ParseChildren(ChildrenAsProperties = false) ]
For the second, you want ChildrenAsProperties=true, and a TabTemplate property of type ITemplate. There's some plumbing involved after that, which this MSDN sample describes. It doesn't add a lot of value if you only need one template, though.

Resources