My apologies in advance for posting such a lengthy question. Believe it or not, what you see here actually represents a fairly condensed version of the problem/code at hand. And while I would appreciate any pointers on a better or different approach, I would also very much like to get the bottom of this so that I can sleep at night :)
I came across a requirement to pass confirmation messages between distinct aspx pages. I opted against using a query string variable since query string values "are" sticky (i.e. they persist on all subsequent postbacks) and I didn't want to deal with adding a bunch of conditional logic around this.
Anyway, I came up with a very simple class that uses Session to associate notifications with specific URLs. I then hooked my master page Page_Load event to query this class for any notifications that should be displayed for the current URL. If it finds any, it dynamically loads a NotificationMessage user control and displays the message content.
Everything works as expected when trying to pass Notifications between different aspx pages. Predictably, things don't work when a content page attempts to add a notification to itself (i.e. "The data you entered is not valid, try again"). The reason is pretty clear: by the time a content page adds a Notification for itself, the Page_Load event of the master page has already fired, so it's too late in the page lifecycle to do any good. The relevant code is pasted below.
public class MyMasterPage:MasterPage{
protected void Page_Load(object sender, EventArgs e)
{
LoadNotifications(this.Request.Url.ToString());
}
private void LoadNotifications(string url)
{
//look for a notification
Notification? notification = NotificationManager.Instance.RetrieveNotification(url);
//there are no notifications, nothing to see here
if (!notification.HasValue)
{
return;
}
//there is a Notification for this url, so load it into a user control
NotificationMessage notificationMessageControl = (NotificationMessage)LoadControl("~/App_UserControls/NotificationMessage.ascx");
notificationMessageControl.ID = "notificationMessage";
notificationMessageControl.Notification = notification;
notificationMessageControl.Visible = true;
//find the placeholder on the master page
PlaceHolder placeHolder = (PlaceHolder)PageUtils.FindControlRecursive(this, "NotificationPlaceholder");
if (placeHolder == null)
{
throw new ApplicationException("NotificationPlaceholder control not found.");
}
//insert into control
placeHolder.Controls.Add(notificationMessageControl);
placeHolder.Visible = true;
//remove the notification so it doesn't show up next time
NotificationManager.Instance.RemoveNotification(url);
}
}
Given the lifecycles issued alluded to above, I modified the NotificationManager class so that it raises an event whenever a notification has been added for the current page. The master page intercepts that event, and if the Page_Load has already fired, it kicks off the LoadNotifications method all over again.
//bind the event on the page constructor
public MyMasterPage()
{
NotificationManager.Instance.NotificationAdded += this.NotificationAdded;
}
private void NotificationAdded(string forUrl)
{
if (_pageLoaded){
LoadNotifications(forUrl);
}
}
Unfortunately, this doesn't work. I have stepped through this code numerous times, and despite the fact that the master page loads the NotificationMessage UserControl and adds it to the appropriate placeholder without incident, the final aspx HTML never includes the markup for that UserControl. I've put breakpoints inside the Page_Load of the UserControl and verified that they are indeed being hit during execution.
If I dynamically load the UserControl from inside the content page and bypass the Master page altogether, it renders without a hitch:
public partial class MyContentPage:Page
{
public void DoSomethingCool(object sender, EventArgs e)
{
if (MyServiceLayer.Save(foo)==false){
Notification notification = new Notification(NotificationType.Error, "We’re sorry, your document was not saved.");
NotificationMessage notificationMessage = (NotificationMessage)LoadControl("~/App_UserControls/NotificationMessage.ascx");
notificationMessage.Notification = notification;
notificationMessage.Visible = true;
PlaceHolder holder = (PlaceHolder)PageUtils.FindControlRecursive(this, "NotificationPlaceholder");
holder.Controls.Add(notificationMessage);
}
}
}
For the record, I stripped out the dynamic loading of the UserControl, opting instead for a a static declaration in the master page markup and a code based toggle of the control's Visible property; still no dice!
If someone could shed some light on this conundrum, I would be much obliged.
I've tried this sort of thing before and I was never fully comfortable with it. What I did instead, was put my ASCX on every page (or on the masterpage), and let the ASCX control its state rather than letting the ASPX control my ASCX.
I'm not sure this will help for your situation, though.
It seems like you want your information to show up after your control events fire. You might consider letting those messages aggregate until all control events have fired and then pull out all of the messages from your NotificationManager in OnPreRender, rather than Page_Load. That way you can get rid of the events (like NotificationAdded), etc that are probably complicating matters.
Not 100% what the problem is, though. It sometimes helps to know that the MasterPage is actually a control on the Page, rather than the other way around like you would think. It is going to be subject to the limitations any control would have on a Page.
HTH, Anderson
Related
I have a page and clicked on the button there it will open a new page containing some text boxes, user fill all the text boxes and clicked the button now first page open again and the question is : How can I get the vales of text boxes on the current page using both server-side and client-side
There is a restrictions to use of :
- Cross-paging
- Cookies
- Sessions
- Query strings
Server-side approach:
An alternate approach is to use Server.Transfer method.
On the current page:
protected void Transfer_Click(object sender, EventArgs e)
{
if (Page.IsValid)
{
Server.Transfer("destination.aspx");
}
}
On the destination page:
protected void Page_Load(object sender, EventArgs e)
{
if (PreviousPage != null)
{
TextBox textBox = PreviousPage.FindControl("Parameter")
as TextBox;
if (textBox != null)
{
string parameter = textBox.Text;
Parameter.Text = parameter;
}
}
}
But Server.Transfer does come with disadvantages. The most serious is that the URL in the browser does not change. The browser still believes it has posted back and received content for the first web form, so history and book-marking suffer.
Client-side approach:
In real world I don't recommend to use this solution, this is only a workaround.
In two words: use window.name property on the client side. This property is available across page reloads it is a sort of session.
For more information see:
What’s in a window.name?
window.name Transport
Hope, this helps.
If you are using the PostBackUrl property of the button on the submitting page, you can access the controls for the "previous page" from the action page by using the following:
Dim txtBox as TextBox
txtBox = CType(Page.PreviousPage.FindControl("MyTextBox"), TextBox)
Then you'll have access programmatically to all of the properties and data for that control.
Use the cache if you can't use the Session, Querystring, Cookies and Cross Page posting.
If it is simple/primitive data, then you can go for the ViewState. Or Cache is a better option.
One more option is to have a Page level public variables, set the required values and redirect to the page for further processing.(This is not a good approach to follow)
I have a GridView with dynamically created image buttons that should fire command events when clicked. The event handling basically works, except for the very first time a button is clicked. Then, the postback is processed, but the event is not fired.
I have tried to debug this, and it seems to me, that the code executed before and after the first click is exactly the same as for any other clicks. (With the exception that in the first click, the event handler is not called.)
There is some peculiarity in that: The buttons which fire the event are created dynamically through databinding, i.e. databinding must be carried out twice in the page lifecycle: Once on load, in order to make the buttons exist (otherwise, events could not be handled at all), and once before rendering in order to display the new data after the events have been processed.
I have read these posts but they wouldn't match my situation:
ASP.NET LinkButton OnClick Event Is Not Working On Home Page,
LinkButton not firing on production server,
ASP.NET Click() event doesn't fire on second postback
To the details:
The GridView contains image buttons in each row. The images of the buttons are databound. The rows are generated by GridView.DataBind(). To achieve this, I have used the TemplateField with a custom ItemTemplate implementation. The ItemTemplate's InstantiateIn method creates the ImageButton and assigns it the according event handler. Further, the image's DataBinding event is assigned a handler that retrieves the appropriate image based on the respective row's data.
The GridView is placed on a UserControl. The UserControl defines the event handlers for the GridView's events. The code roughly looks as follows:
private DataTable dataTable = new DataTable();
protected SPGridView grid;
protected override void OnLoad(EventArgs e)
{
DoDataBind(); // Creates the grid. This is essential in order for postback events to work.
}
protected override void Render(HtmlTextWriter writer)
{
DoDataBind();
base.Render(writer); // Renews the grid according to the latest changes
}
void ReadButton_Command(object sender, CommandEventArgs e)
{
ImageButton button = (ImageButton)sender;
GridViewRow viewRow = (GridViewRow)button.NamingContainer;
int rowIndex = viewRow.RowIndex;
// rowIndex is used to identify the row in which the button was clicked,
// since the control.ID is equal for all rows.
// [... some code to process the event ...]
}
private void DoDataBind()
{
// [... Some code to fill the dataTable ...]
grid.AutoGenerateColumns = false;
grid.Columns.Clear();
TemplateField templateField = new TemplateField();
templateField.HeaderText = "";
templateField.ItemTemplate = new MyItemTemplate(new CommandEventHandler(ReadButton_Command));
grid.Columns.Add(templateField);
grid.DataSource = this.dataTable.DefaultView;
grid.DataBind();
}
private class MyItemTemplate : ITemplate
{
private CommandEventHandler commandEventHandler;
public MyItemTemplate(CommandEventHandler commandEventHandler)
{
this.commandEventHandler = commandEventHandler;
}
public void InstantiateIn(Control container)
{
ImageButton imageButton = new ImageButton();
imageButton.ID = "btnRead";
imageButton.Command += commandEventHandler;
imageButton.DataBinding += new EventHandler(imageButton_DataBinding);
container.Controls.Add(imageButton);
}
void imageButton_DataBinding(object sender, EventArgs e)
{
// Code to get image URL
}
}
Just to repeat: At each lifecycle, first the OnLoad is executed, which generates the Grid with the ImageButtons. Then, the events are processed. Since the buttons are there, the events usually work. Afterwards, Render is called, which generates the Grid from scratch based upon the new data. This always works, except for the very first time the user clicks on an image button, although I have asserted that the grid and image buttons are also generated when the page is sent to the user for the first time.
Hope that someone can help me understand this or tell me a better solution for my situation.
A couple problems here. Number one, there is no IsPostBack check, which means you're databinding on every load... this is bound to cause some problems, including events not firing. Second, you are calling DoDataBind() twice on every load because you're calling it in OnLoad and Render. Why?
Bind the data ONCE... and then again in reaction to events (if needed).
Other issue... don't bind events to ImageButton in the template fields. This is generally not going to work. Use the ItemCommand event and CommandName/CommandArgument values.
Finally... one last question for you... have you done a comparison (windiff or other tool) on the HTML rendered by the entire page on the first load, and then subsequent loads? Are they EXACTLY the same? Or is there a slight difference... in a control name or PostBack reference?
Well I think the event dispatching happens after page load. In this case, its going to try to run against the controls created by your first data-binding attempt. This controls will have different IDs than when they are recreated later. I'd guess ASP.NET is trying to map the incoming events to a control, not finding a control, and then thats it.
I recommend taking captures of what is in the actual post.
ASP.NET is pretty crummy when it comes to event binding and dynamically created controls. Have fun.
Since in my opinion this is a partial answer, I re-post it this way:
If I use normal Buttons instead of ImageButtons (in the exact same place, i.e. still using MyItemTemplate but instantiating Button instead of ImageButton in "InstantiateIn", it works fine.
If I assert that DoDataBind() is always executed twice before sending the content to the client, it works fine with ImageButtons.
Still puzzled, but whatever...
I've got a rather lengthy question I'm afraid. I'm fairly new to ASP.NET so please bear with me.
I have built a control for an ASP.NET page that lists a number of options. Each option has two clickable areas (call them buttons for the sake of simplicity). One to select the option and one to hide the option.
protected void Page_Load(object sender, EventArgs e)
{
RenderOptions();
}
public void RenderOptions()
{
for (int i = 0; i < 5; i++) {
HtmlGenericControl div1 = new HtmlGenericControl("div");
div1.Attributes.Add("onclick", ClientScript.GetPostBackEventReference(this, "option" + i));
m_TreeContainer.Controls.Add(div1);
HtmlGenericControl div2 = new HtmlGenericControl("div");
div2.Attributes.Add("onclick", ClientScript.GetPostBackEventReference(this, "option" + i));
m_TreeContainer.Controls.Add(div2);
}
}
public void RaisePostBackEvent(string arg)
{
//do something
}
This works fine (I do implement the IPostBackEventHandler interface). The problem here is that there doesn't seem to be a way for me to find which HTML element was clicked and thus which action should be performed in the RaisePostBackEvent method.
What I tried to do is create a new class (HtmlDivControl) which looks like this:
class HtmlDivControl : HtmlGenericControl, IPostBackEventHandler
{
#region Delegates
public delegate void ClickEventHandler(object sender, string eventArgument);
#endregion
#region Properties
private ClickEventHandler m_Click;
public ClickEventHandler Click
{
get { return m_Click; }
set { m_Click = value; }
}
#endregion
#region Constructors
public HtmlDivControl()
{
}
#endregion
public void RaisePostBackEvent(string eventArgument)
{
m_Click.Invoke(this, eventArgument);
}
}
Now I made div1 and div2 my HtmlDivControl rather than HtmlGenericControl, set the Click property to a method (delegate) and passed the div (div1 or div2) itself as control for the GetPostBackEventReference method. This time, I could not only differentiate between the divs but also pre-determine the action that should be performed. However, the RaisePostBackEvent for controls are called after PageLoad. So the problem I'm with now is that the whole options control is rendered before the events are handled (and thus, an option that should for instance be hidden isn't because the actual hiding happens after the rendering). Moving the RenderOptions() call to the PageLoadComplete method doesn't help either, since then the div controls won't exist yet.
I'm pretty sure I'm missing something quite fundamental here. But could someone please explain me how I should approach something like this?
p.s.
How am I supposed to write underscores here? They're used to make text italic? Is there some escape character?
For someone new to ASP.Net, you've done pretty well so far. Your roadblock here is actually the way you are thinking about the issue. You should get a good grasp of the ASP.Net Page Lifecycle - you are missing something very fundamental.
In a nutshell, you want your page to rebuild it's state to the same way it was before the postback. Then process your events. Then make any state changes.
You're thinking about it as if your html controls should know about their state change at the start of the request, which is incorrect. There has to be the rebuilding phase first. This is critical for ASP.Net to even figure out which events to raise.
What I would recommend:
move your "RenderOptions()" method to the Page_Init handler. This will save you lots of issues if you ever incorporate ViewState into your controls. (I would also rename it, as it's not truly rendering anything, it's just adding your controls to the page. Render has a specific context in ASP.Net).
Then, in your OnClick event handlers for your controls, simply set your controls visibility as necessary, rather than trying to control the way they are rendered. It is always much simpler to set controls to Visible=False rather than try to change the way the controls are being rendered to the page. Remember that if you set Visible=False, there will be zero html sent to the response for that control, but the server will still know it's on the page, so you can still deal with it.
Think about your event handlers as the place where you will change the state of the page. It's where your logic should be in this case, rather than in Page_Load.
I have a gridview button that I programmatically created and I want to load an update panel on the client side with the sent data. I have a hidden value field that gets its data on the click of the gridview button and the dropdownlist in my updatepanel depends on that value.
while calling __doPostBack directly will work, it's not a perfect solution because the name of that function is strictly speaking an implementation detail of the .Net framework.
A better solution is to use ClientScriptManager.GetPostBackEventReference, which gives you a more resilient interface to the same functionality. Do note that GetPostBackEventReference and GetCallBackEventReference are not the same thing - the former causes a page reload (partial or full, depending on how your UpdatePanels are set up), while the latter doesn't.
The easiest way to do this is to call __doPostBack from client side.
On client side button1_onclick method, calls:
__doPostBack('<%=UpdatePanel1.ClientID %>','Refresh:0,1,2'); //refresh update panel
On page behind add the following event handler to capture the post back call:
protected void UpdatePanel1_Load(object sender, EventArgs e)
{
string arg = Request.Form["__EVENTARGUMENT"];
if (string.IsNullOrEmpty(arg)) return;
if (arg.StartWith("Refresh")
{
//parse data first then do your thing here...
}
}
And of course don't forget to wire event to the above method:
protected void Page_Init(object sender, EventArgs e)
{
UpdatePanel1.Load += new EventHandler(UpdatePanel1_Load);
}
we use the __dopostback() method which simulates a postback and causes the updatepanel to refresh
__doPostBack('controlName','');
Don't forget that the control name is it's HTML ID (which may well contain dollars etc) and not just it's ASP.NET ID.
As far as I know you can either call this method and pass in the hidden value field, or the div that it is in.
I am writing a custom HTTP handler to provide an edit form for a grid using existing user controls. Basically, my handler creates the page, form, header, and other controls necessary to be able to render the existing user controls properly and at the end of ProcessRequest, I use Server.Execute to execute the page that was dynamically created. I am doing this because the solution where this resides is a user controls project and there are no pages, nor can we add any. This needs to be reusable for several projects.
This works great up until the point where the user controls added to this "page" require the usage of the postback mechanism. In the user control Page.IsPostBack is always false and control events (like a button click) are not handled. It is obvious that I am missing some critical piece from how a typical ASP.NET page works. The Page class is just an implementation of an IHttpHandler, but there is a lot of code that I don't think should be necessary to get the basic functionality to work here.
Any ideas?
Here's the basic code from my base HTTP handler. I have other classes that inherit from this base handler to add the actual user controls to the form of the page.
public void ProcessRequest(HttpContext context) {
context.Response.ContentType = "text/html";
HtmlGenericControl htmlPage = GetHtml();
AddTitle();
htmlPage.Controls.Add(_head);
HtmlGenericControl htmlBody = GetBody();
_form.Action = context.Request.Url.ToString();
_form.Method = "POST";
htmlBody.Controls.Add(_form);
htmlPage.Controls.Add(htmlBody);
AddAjaxManager();
AddScriptManager();
_page.Controls.Add(htmlPage);
//_page.ProcessRequest(context);
context.Response.CacheControl = "No-Cache";
context.Server.Execute(_page, context.Response.Output, true);
}
public bool IsReusable { get { return false; } }
To make this work, I inherited from Page instead of implementing IHttpHandler. You do still need to build out the entire HTML of the page, but you get all the wonderfulness (or not) of the ASP.NET WebForms page lifecycle when you do this.