Storing and restoring properties in ASP.NET derived control - asp.net

I have created an ASP.NET class derived from the standard WebControls.TextBox, with the intention of adding extra properties that will persist between post-backs. However, I cannot figure how to get the values in these properties to persist.
I have tried setting the value of the properties into the controls ViewState as part of the PreRender handler, but the value is then not accessible in the Init handler on the post-back, because the ViewState has not yet been setup.
I could look for the ViewState value in the Load handler of the control, but if the page/usercontrol that is using the control asks for the properties value during its Load handler, the control hasn't yet reached it's Load handler, and it therefore not there.
My current class looks something like this...
Public Class MyTextBox
Inherits TextBox
Private _myParam As String = ""
Public Property MyParam As String
Get
Return _myParam
End Get
Set(value As String)
_myParam = value
End Set
End Property
Private Sub MyTextBox_Init(sender As Object, e As EventArgs) Handles Me.Init
If Page.IsPostBack Then
_myParam = ViewState("myParam")
End If
End Sub
Private Sub MyTextBox_PreRender(sender As Object, e As EventArgs) Handles Me.PreRender
ViewState("myParam") = _myParam
End Sub
End Class
I must be missing something really simple, such as whether there is an attribute I can set against the property.
UPDATE
Thanks to #AVD pointing out that I really had very little clue about the ViewState and the Init / Load stages, I finally figured it all out.
If you have the time (and if you're doing any major ASP.NET work, you need to make the time) please read the Understand ASP.NET View State document that #AVD pointed me to. It will explain a lot.
However, what it didn't explain is if you place your control within a <asp:Repeater>, you may as well throw all the rules out of the window... and that is exactly the problem I was experiencing.
In the end, the way I managed to get it to work was to use a <asp:PlaceHolder> control within the repeater, create an instance of my control within the ItemDataBound handler of the repeater, and then add the control to the <asp:PlaceHolder>... all done within the Init section (which fortunately I'm able to do).
As Andrew found out in this previous question you can end up in a chicken/egg situation, where you need to create the controls in the Init, but you won't know what controls you need until the Load.
(I have still made AVD's answer the correct one, because in the context of my original question, it is absolutely correct).

You have to store/retrieve value to/from ViewState within the properties accessors.
Public Property MyParam As String
Get
If IsNothing(ViewState("myParam")) Then
return string.Empty
End IF
Return ViewState("myParam").ToString()
End Get
Set(value As String)
ViewState("myParam") = value
End Set
End Property

Related

What type of variable could I use for this?

What I am trying to do is simple in principle, but the lifecycle of ASP.NET pages is throwing a bucket of cold water into my day.
Here is the problem
We have implemented URL Redirection and I've inherited code that reads like this in Global.ASAX, on Sub Application_BeginRequest:
dv.Table = CommonFunctions.ConvertXmlFileToDataSet("/XmlData/WebAppFolders.xml").Tables("Application")
dv.RowFilter = "'" + fullOrigionalPath + "' LIKE '%'+ folder + '%'"
If dv.Count > 0 Then 'match on key to redirect
If fullOrigionalPath.EndsWith(dv(0)("folder") + "/") Then
Context.RewritePath(dv(0)("basePage"), True)
Else 'missing page in directory --> redirect
HttpContext.Current.Items("Raise404") = "true"
Response.Redirect("/" + dv(0)("folder"))
End If
Return
End If
Basically we are reading a large XML file that contains the URL redirects. That's working fine. The problem happens in the line...
HttpContext.Current.Items("Raise404") = "true"
In the context of Application_BeginRequest, the session object is not yet available, so I could not use it to store a flag which I named Raise404. Instead I had to resort to using the Items collection.
The problem occurs when the redirect takes place. The new page lifecycle destroys the Items array and overwrites it with a new empty one.
By the time I try to use my flag Raise404, it no longer exists on my page PreRender event.
To complicate matters we use master pages, and so I was asked to place the code that we want to execute in the master page
Ideally, if the Items array wasn't being destroyed, this code would work:
Private Sub Page_PreRender(sender As Object, e As System.EventArgs) Handles Me.PreRender
If HttpContext.Current.Items("Raise404") IsNot Nothing AndAlso HttpContext.Current.Items("Raise404").Equals("true") Then
Response.StatusCode = 404
HttpContext.Current.Items("Raise404") = Nothing
End If
End Sub
I am not sure what kind of variable I could use to store my flag and allow it to survive the redirect.
Any ideas?
Update: The problem is that the HTTP Handler servicing my request is System.Web.DefaultHTTPHandler, which does not implement IRequiresSessionState, and so when my request is being handled inside Global ASAX there is no session created. So, it seems like the solution will be to write a custom HTTPHandler that implements IRequiresSessionState, and use that for all my .aspx files. Even then, a session state is not created in Global.ASAX until PreRequestHandlerExecute is raised. So, putting it all together I think I need to write a custom HTTP Handler that implements IRequiresSessionState, and delay the redirection of the page until PreRequestHandlerExecute is raised where I will be able to store my flag into the Session state, and only after that, redirect my page.
Not very elegant, and I wonder if there will be any performance implications.
Have you considered using the query string for that?
Response.Redirect("/" & dv(0)("folder") & "?Raise404=true")
Then, in your Master Page, simply check QueryString("Raise404") and act accordingly.
The only drawback I can see is that a malicious client could deliberately add Raise404=true to the query string, which is not possible with your current solution. However, I do not see how that could do any harm.
As I suspected, my problem was directly tied to the fact that IIS 8 will avoid using an HTTPHandler that implements IRequiresSessionState if for performance reasons it detects that a session object is not needed, and so because the redirect code was happening during the Application_BeginRequest event, IIS was handling up to that point my requests using System.Web.DefaultHTTPHandler.
So, upon further research, I found this which was tremendous help:
IRequiresSessionState - how do I use it?
And I also documented myself on this article:
http://forums.iis.net/t/1094546.aspx
What solved my problem was to write a dummy HTTPHandler class. I added a new class to my app_code folder:
Imports System.Web
Public Class HTTPRequestHandler : Implements IHttpHandler, IRequiresSessionState
Private OriginalHandler As IHttpHandler
Public Sub New(handler As IHttpHandler)
Me.OriginalHandler = handler
End Sub
Public Sub ProcessRequest(context As HttpContext) Implements IHttpHandler.ProcessRequest
Throw New InvalidOperationException("HTTPRequestHandler cannot process requests.")
End Sub
Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
Get
Return True
End Get
End Property
Public ReadOnly Property Handler() As IHttpHandler
Get
If Me.OriginalHandler IsNot Nothing Then
Return Me.OriginalHandler
End If
Return HttpContext.Current.Handler
End Get
End Property
End Class
Now, during Application_BeginRequest, session is not available but HttpContext.Current.Items is available. So, as the second article suggests, during PostMapRequestHandler I put a condition:
If HttpContext.Current.Items("Raise404") IsNot Nothing Then
Context.Handler = New MyNameSpace.HTTPRequestHandler(Context.Handler)
End If
And finally, that created the session object that I could finally use during PreRequestHandlerExecute:
HttpContext.Current.Session("Raise404") = "true"
Response.Redirect(HttpContext.Current.Items("Redirect"))
I will definitely not argue that this is not elegant, but it works and I can raise my 404 error during Page_PreRender on my master page.
I hope this may help others.
Cheers.

Trying to get Session value in Global.asax, but it is NULL

I have a session which contains a List(of String) which I create on Main.aspx. In my Global.asax I execute the following code to retrieve the Session value after a Timer is fired.
Private Sub Application_AcquireRequestState(sender As Object, e As EventArgs)
If System.Web.HttpContext.Current.Session IsNot Nothing Then
If Session("Products") IsNot Nothing Then
Response.Write(Session("Products").ToString())
End If
End If
End Sub
However, Session("Products") returns Null and the Sub is exited. I wonder if End Sub in Main.aspx releases the Session and therefore whenever the Timer fires, the Session is empty. Any thoughts?
In your Main.vb code behind, be sure to inherit the System.Web.SessionState.IRequiresSessionState interface in addition to whatever is being inherited.
Something like this should work:
Public Class Main
Inherits System.Web.UI.Page, System.Web.SessionState.IRequireSessionState
' Your application code
End Class
This is strange behavior though as ASP.NET webpages should already handle session data. Usually, you inherit IRequireSessionState on generic handlers (.ashx) since they don't support session data by default.

How to use a viewstate'd object as a datasource for controls on a user control

I've got a listview on a control. Each row comprises a checkbox and another listview.
The outer listview is bound to a property on the control (via a method call, can't set a property as a SelectMethod on an ObjectDataSource it would appear) which is lazy loaded suchly:
Public ReadOnly Property ProductLineChargeDetails() As List(Of WebServiceProductLineChargeDetail)
Get
If ViewState("WSProductLineChargeDetails") Is Nothing Then
ViewState("WSProductLineChargeDetails") = GetWebServiceProductLineChargeDetails()
End If
Return DirectCast(ViewState("WSProductLineChargeDetails"), Global.System.Collections.Generic.List(Of Global.MI.Open.WebServiceProductLineChargeDetail))
End Get
End Property
The shape of the object referenced by the data source is something like this:
(psuedocode)
Product
{
bool Licenced;
List<Charge> charges;
}
Charge
{
int property1;
string property2;
bool property3
.
.
.
}
The reason for the use of viewstate is this:
When an one of the checkboxes on one of the outer list view rows is checked or unchecked I want to modify the object that the ODS represents (for example I'll add a couple of Charge objects to the relevant Product object) and then rebind.
The problem I'm getting is that after every postback (specifically after checking or unchecking one of the rows' checkbox) my viewstate is empty. Thiss means that any changes I make to my viewstate'd object is lost.
Now, I've worked out (after much googling and reading, amongst many others, Scott Mitchel's excellent bit on ViewState) that during initial databinding IsTrackingViewState is set to false. That means, I think, that assigning the return from GetWebServiceProductLineChargeDetails() to the ViewState item in my Property Get during the initial databind won't work.
Mind you, even when the IsTrackingViewState is true and I call the Property Get, come the next postback, the viewstate is empty.
So do you chaps have any ideas on how I keep the object referenced by the ObjectDataSource in ViewState between postbacks and update it and get those changes to stay in ViewState? This has been going on for a couple of days now and I'm getting fed up!
Cheers in advance
Steve
In order for changes to be tracked in the viewstate the object must be assigned to the viewstate when IsTrackingViewState is True. Viewstate starts tracking after Init in the pagelifecycle. You are probably binding too early for the objects to be stored in viewstate. You should bind the list view after the Init stage, probably in the load stage.
i.e.
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
ListView.DataSource = ProductLineChargeDetails
ListView.DataBind
End Sub

Why is the DataBind() method necessary?

Simple question, I guess.
For a long time I've blindly followed a (supposedly) common pattern when programmatically databinding my ASP.NET controls. Namely:
gridView1.DataSource = someList;
gridView1.DataBind();
However, if I were setting my GridView to bind to a DataSource control via the DataSourceID property, the call to DataBind() is unnecessary. Namely:
gridView1.DataSourceID = LinqDataSource1;
is sufficient.
Furthermore, if you try to set the DataSource property in ASPX markup, you are greeted with the following:
You cannot set the DataSource property declaratively.
I assume these are related, but I am still stumped as to why DataBind() is necessary. The difference between DataSource and DataSourceID is secondary - I can understand some magic taking place there. The real question is why doesn't the DataSource propery setter cause databinding automatically? Are there any scenarios in which we want to set the DataSource but not bind to it?
In ASP.Net, it's often important to have certain data available and ready at certain points in the page life cycle, and not before. For example, you may need to bind to a drop down list early to allow setting the selected index on that list later. Or you might want to wait a bit to bind that large grid to reduce the amount of time you hold that connection active/keep the data in memory.
Having you explicitly call the .DataBind() method makes it possible to support scenarios at both ends of the spectrum.
DataSource is a property of the BaseDataBoundControl class. DataSourceID is a property of the DataBoundControl class, which inherits from BaseDataBoundControl and did not exist before ASP.NET 2.0.
Since DataBoundControl is explicitly for displaying data in a list or tabular form, and BaseDataBoundControl cannot make that assumption, binding is not automatic when DataSource is set because the type of control may not match the structure of the data.
Of course, this is all just a guess based on the MSDN documentation, so I could be wrong.
I noticed that without using DataBind() that nothing will be displayed in my GridView so I always include it as shown in this section of code:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
' TableAdapter object.
' Provide communication between this application and the database.
'-----------------------------------------------------------------
Dim suppliersAdapter As New SuppliersTableAdapter
' Get the data from the TableAdapter into the GridView.
'------------------------------------------------------
GridView1.DataSource = suppliersAdapter.GetSuppliers()
' Display the result set from the TableAdapter in the GridView.
'--------------------------------------------------------------
GridView1.DataBind()
End Sub
Please forgive the extra commenting as I'm also still learning ASP.Net as well and the comments will help me learn better "what and why" to use certain statements.
Try this:
if (GridView1.EditIndex == e.Row.RowIndex)
{
TextBox t2 = (TextBox)e.Row.FindControl("TextBox2");
DateTime dt2;
if (DateTime.TryParse(t2.Text, out dt2))
{
t2.Text = dt2.ToString("yyyy-MM-dd");
}
}

How can I share a variable from sub routine to sub routine?

I have a sub routine where I pull in information from a database. I want to use variables from this sub routine in another sub routine. I tried making everything public but it doesn't want to be friendly and share.
Dim strEmail as String
Public Sub readDB()
strEmail = "whatever#doohikcy.com"
End Sub
Public Sub submit_btn(ByVal sender As Object, ByVal e As EventArgs)
Response.Write(strEmail)
End Sub
The readDB would read the database and set the variables and do whatever it needs to do. Then when they submit the form it would e-mail the form to whatever the email address was.
Every time an asp.net page loads, the global variables are wiped out. You can avoid this by using Session variables.
Public Sub readDB()
Session("strEmail") = "whatever#doohikcy.com"
End Sub
Then:
Public Sub submit_btn(ByVal sender As Object, ByVal e As EventArgs)
Response.Write(CStr(Session("strEmail")))
End Sub
Passing them as arguments is great, if the sequence of calls to the subs in question makes that feasible. If you literally need a variable that is visible to different subs in the same class (code-behind page in ASP.Net), what you are looking for is probably a private member variable. Declare it outside of any sub or function with the access modifier Private, and all the subs in the class will be able to access it.
Private _foo As String
The underscore is a convention that some people love, some hate. It comes in handy in VB if you want to define a property to expose the variable, where you can't use Foo as distinctly different from foo, but that's another story.
This is not the same as what would generally be understood by the term Global variable in the ASP.Net sense, where the variable would be visible throughout the application context, which lends itself to unintended consequences in the least. A private member variable is only visible to the class that owns it.
EDIT: Your example code was added after my initial answer. My VB is a little rusty but as you've written it, strEmail looks like it should have class-level visibility, including inside of submitbtn (someone correct me if I'm wrong). One possibility, since you mentioned that you are calling readDB in Page_Load is if you're checking for postback in page load, and only calling readDB on the initial load, not on postback, which would be the case when the button is clicked. You may have seen examples that include a check for Postback out of hand and not realized what it does (I only suggest that because you mentioned that you are new to ASP.Net and it's not intuitive if you're new to it--no offense intended).
Protected Sub Page_Load (sender as object, e as EventArgs)
If Not IsPostback
// this doesn't get called when the button is clicked so
// strEmail would not be populated when submitbtn is invoked
readDB
End If
End Sub
That's a bit of a guess out of nowhere though, so it might be way off base. Have you set break-points in Page_Load, readDB and submitbtn to see the state of strEmail in each?

Resources