ViewState error with dynamically built menu - asp.net

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.

Related

RaisePostBackEvent called on wrong control

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.

Get e parameter from a button that hasn't been clicked

Is there a way that I could access the e event arguments for a button that has not been clicked?
I need to delete multiple entries in a gridview by clicking a button and having it simulate clicking the delete button for each selected entry, but I can't use performClick, so I'm trying to call the actual method that deletes each one. However, that method requires an "e As System.Web.UI.WebControls.GridViewCommandEventArgs" parameter and I can't figure out how to get that.
You won't be able to access the EventArgs parameter.
I'd suggest you design your code like this:
public class MyClass
{
private ListView listView;
protected void OnClick(EventArgs e)
{
performAction();
}
private void performAction()
{
listView.deleteSelectedItems();
}
}
Don't implement functionality you are going to need somewhere else in delegates. Instead call this functionality inside the delegates' body. This way you can reuse performAction() somewhere else ..
Your problem calling delete button can be resolved if you add one check box in each row of datagrid and on click of button Delete you can perform delete operation for the checked rows in following manner
Protected void btnDelete_Click(object sender, EventArgs e)
{
for(int i = 0; i < GridView1.Rows.Count; i++)
{
CheckBox chkDelete = (CheckBox)GridView1.Rows[i].Cells[0].FindControl("chkSelect");
if(chkDelete != null)
{
if(chkDelete.Checked)
{
strID = GridView1.Rows[i].Cells[1].Text;
ids.Add(strID); //ids can colletion of any type
}
}
}
}
Now send ids to any function to perform delete.

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;
}

Silverlight Listbox: Binding does not get updated

I have a Wizard control that has multiple steps. Controls that are not visible get removed from the visual tree. I have a ListBox on one page, that binds to an ObservableCollection<T>. When items get added or removed to that ListBoxon one page, the ListBox on another page (with the same ItemsSource), the binding on the other page does not get updated. I hope this explains my problem clearly enough.
How do I get this binding to update when the page gets added to the visual tree again ?
I cannot reproduce your problem. I was able to remove a ListBox from the visual tree, add objects to the ObservableCollection, and when I add it to the visual tree, items are actually updated.
Try working around your problem by setting visibility to Collapsed rather than removing from Visual Tree.
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.listBox1.ItemsSource = o;
this.listBox2.ItemsSource = o;
}
ObservableCollection<int> o = new ObservableCollection<int>();
private void buttonList1_Click(object sender, RoutedEventArgs e)
{
if (this.listBox1.Parent == null)
this.LayoutRoot.Children.Add(this.listBox1);
else
this.LayoutRoot.Children.Remove(this.listBox1);
//this.listBox1.Visibility = this.listBox1.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
private void buttonList2_Click(object sender, RoutedEventArgs e)
{
if (this.listBox2.Parent == null)
this.LayoutRoot.Children.Add(this.listBox2);
else
this.LayoutRoot.Children.Remove(this.listBox2);
//this.listBox2.Visibility = this.listBox2.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
private void ButtonAddMore_Click(object sender, RoutedEventArgs e)
{
o.Add(o.Count);
}
}

DropDownList OnSelectedIndexChange to 0th index w/out ViewState

I did follow the article TRULLY Understanding ViewState (great article btw) and populating my drop down list is working great. I've even setup a OnSelectedIndexChange event which fires almost as great.
The problem I've found is the SelectedIndexChanged event won't fire when selecting the 0th index. It does all other times however.
Here's some code:
<asp:DropDownList runat="server" ID="DropDownList1" EnableViewState="false"
AutoPostBack="True" OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" />
protected override void OnInit(EventArgs e)
{
this.DropDownList1.DataTextField = "Text";
this.DropDownList1.DataValueField = "Value";
this.DropDownList1.DataSource = fillQueueDropDown();
this.DropDownList1.DataBind();
base.OnInit(e);
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
OnSelectedQueueChanged(e);
}
public void OnSelectedQueueChanged(EventArgs e)
{
// Do stuff.
}
public event EventHandler queueNamesChangedEvent;
public void OnSelectedQueueChanged(EventArgs e)
{
if (queueNamesChangedEvent != null)
queueNamesChangedEvent(this, e);
}
I suppose I can do some type of check in the Page_Load method:
if(ViewState["selectedIndexChangedFlag"] != 1)
// raise OnSelectedChange event
Or is there something I can setup in the OnInit() method where I'm rebinding this data everytime that i can do?
See, my custom EventHander raises an event which is caught by a the parent page in which this control resides, so that the parent could take some action using the newly selected value. And this is currently working for all cases where the selected index > 0.
I create a property in this control which contains the most recently selected index, in which case my parent page can action on this property value on every Page_Load... dunno.
Open to suggestions. Or how to force this SelectedIndexChanged event to fire for that 0th index selection.
The problem is that you are loading the data each time and this is resetting the selected index. Imagine this is your dropdown:
zero [selected]
one
two
Then in the client you change the selected index:
zero
one [selected]
two
This populates the hidden input __EVENTARGUMENT with your new index (1) and the hidden input __EVENTTARGET with the id of your dropdown. Now the server-side code kicks in and reloads your data:
zero [selected]
one
two
"zero" is the selected value because that is the default when the data is loaded. Then ASP.NET looks for __EVENTTARGET and __EVENTARGUMENT in the Request and finds your dropdown's id and finds the new index (1). Now your dropdown looks like this:
zero
one [selected]
two
Since the index has changed, the dropdown raises its SelectedIndexChanged event indicating that the index has changed. Obviously this is the part that is working, now lets see why selecting the first item in the list does not raise the event.
Now lets say that we still have the dropdown in the state it was just in (with "one" being selected and the selected index of 1). What happens when we select the first item in the list on the client?
__EVENTTARGET and __EVENTARGUMENT are populated with the id of the dropdown and the new index (0). Then the server loads the data into the dropdown and the dropdown now looks like this again:
zero [selected]
one
two
Notice that since you reloaded the data before the events fired the index is already set to 0 because that is the default. Now when your event fires and the dropdown's selected index is set to 0, the dropdown does not see this as a change since the selected index (as far as it knows) has not changed.
Here is how to fix the problem:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!Page.IsPostBack)
{
this.DropDownList1.DataTextField = "Text";
this.DropDownList1.DataValueField = "Value";
this.DropDownList1.DataSource = fillQueueDropDown();
this.DropDownList1.DataBind();
}
}
What this will do is only load the data into the dropdown if the page is not a postback. This means that ViewState will maintain the data for you as well as the selected index so that when you post back the dropdown will compare the new index to the index you saw in the client.
My goal with disabling the ViewState on this drop down list is to minimize the size of the ViewState for the page.
The problem I had with only doing the if(!Page.IsPostBack){...DataBind()...}, is that when you select an item for the first time, and the page reloads, my drop down list becomes empty.
What I ended up doing was creating another Property on this control, LastIndex. When the OnSelectedIndexChanged event fires, I update the LastIndex value. In the Page_Load, I compare the Current and Last index values, if they're different, then fire a Index changed event.
public int SelectedValue{
get { return this.DropDownList1.SelectedItem.Value; }
}
public int LastIndex{
get { return this.ViewState["lastIndex"] == null ? -1 : (int)this.ViewState["lastIndex"]; }
set { this.ViewState["lastIndex"] = value; }
}
protected override void OnInit(EventArgs e){
base.OnInit(e);
this.DropDownList1.DataTextField = "Text";
this.DropDownList1.DataValueField = "Value";
this.DropDownList1.DataSource = fillQueueDropDown();
this.DropDownList1.DataBind();
}
protected void Page_Load(object sender, EventArgs e){
if (this.LastIndex != this.SelectedValue)
this.OnSelectedQueueChanged(new EventArgs());
}
private ListItemCollection fillQueueDropDown(){...}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e){
OnSelectedQueueChanged(e);
this.LastIndex = this.SelectedValue;
}
public event EventHandler queueNamesChangedEvent;
public void OnSelectedQueueChanged(EventArgs e){
if (queueNamesChangedEvent != null)
queueNamesChangedEvent(this, e);
}
You are right though. The data is re-loaded and re-bound in the OnInit phase. Then the ViewState is restored (and when the 0th index is restored), when we finally get to the Events phase, the control doesn't detect the change.
Not sure this is the most elegant route, but it's working good so far.
Then i found this in the msdn docs for IPostBackDataHandler:
public virtual bool LoadPostData(string postDataKey,
NameValueCollection postCollection) {
String presentValue = Text;
String postedValue = postCollection[postDataKey];
if (presentValue == null || !presentValue.Equals(postedValue)) {
Text = postedValue;
return true;
}
return false;
}
Since the present value is the same as the changed-to value, the event isn't fired.

Resources