I have an ASCX user control that is being used in about 60 web forms pages. This control basically renders a series of nested drop down lists.
Inside the control, the method for populating the final list is like this:
Public Sub PopulateList()
Dim dt as DataTable = MyDAL.GetListValues()
For each dr as DataRow in dt.Rows
Dim li as new ListItem
' ...
myDDL.Items.Add(li)
Next
End Sub
In a small handful of pages, I need this method to work slightly differently (the list items are populated with more details, from a different table).
Is it possible somehow for the parent page of the control to somehow override the method? I read about Overriding on various MSDN pages, but can't figure this out. I can declare the method as...
Public Overridable Sub PopulateList()
... but then in VS2015, when I try to create an overriding method using Public Overrides the Intellisense menu does not contain any reference to the user control, or the method. I assume that this is because the control isn't actually being inherited by the page?
Is this possible to do, or is there another way please?
You cannot "override" the method in the parent page, because the page does not inherit from your control's class.
You could possibly create an event handler, or pass in a delegate to modify the behavior of the method.
For example:
Public Class Test1
Dim t2 As New Test2
Sub New()
' Call populateList with an action handler
t2.PopulateList(Sub(ddl)
' Do your logic here
Dim dt As DataTable = MyDAL.GetListValues()
For Each dr As DataRow In dt.Rows
Dim li As New ListItem
' ...
ddl.Items.Add(li)
Next
End Sub)
End Sub
End Class
Public Class Test2
Public Sub PopulateList(Optional handler As Action(Of DropDownList) = Nothing)
If handler Is Nothing Then
' Default behavior
Dim dt As DataTable = MyDAL.GetListValues()
For Each dr As DataRow In dt.Rows
Dim li As New ListItem
' ...
myDDL.Items.Add(li)
Next
Else
' Invoke action handler and pass a reference to the dropdown you want to add items to
handler(myDDL)
End If
End Sub
End Class
Example using an event:
Event MyCustomEvent As EventHandler(Of MyCustomEventArgs)
Public Sub PopulateList()
Dim args As New MyCustomEventArgs()
args.ListObject = myDDL
RaiseEvent MyCustomEvent(Me, args)
' Do default behavior if not handled by event code
If Not args.Handled Then
Dim dt As DataTable = MyDAL.GetListValues()
For Each dr As DataRow In dt.Rows
Dim li As New ListItem
' ...
myDDL.Items.Add(li)
Next
End If
End Sub
Custom event args class:
Public Class MyCustomEventArgs
Inherits EventArgs
Public Property Handled As Boolean
Public Property ListObject As DropDownList
End Class
Handled on your page:
Protected Sub MyControl_MyCustomEvent(sender As Object, e As MyCustomEventArgs) Handles MyControl.MyCustomEvent
e.Handled = True
' Do work on your list
Dim mylist = e.ListObject
End Sub
The purpose of my detailsview is to collect report parameters for the user to enter when running a report. So I build the details view dynamically because report parameters are data driven, stored in the database per the report.
The object being bound to is created by reflection dynamically at run-time from the report parameters, with one property per each parameter, so it contains simple date or collection properties, which I turn into a combo box or date box dynamically by creating templates dynamically.
In the item_updating event, I simply want to get the values the user entered so I know how to run the report.
I looked in all the properties of e and also tried findcontrol to get to the values the user entered. both e and findcontrol are empty. This is how I got data back in the past so I don't know why they aren't there. It almost as if the controls themselves are not represented in the object model of the detailsview at that time.
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="false" DefaultMode="Edit" >
<Fields>
<asp:CommandField ButtonType="Button" UpdateText="Run" ShowCancelButton="false" ShowEditButton="true" />
</Fields>
</asp:DetailsView>
code behind
Private Sub BuildDetailView(DataSource As Object)
Dim Properties() As System.Reflection.PropertyInfo = DataSource.GetType.GetProperties
Dim Template As System.Web.UI.ITemplate
For Each PropertyInfo As System.Reflection.PropertyInfo In Properties
Template = Nothing
If PropertyInfo.PropertyType Is GetType(System.DateTime) Then
Template = New DateTemplate(DataSource, PropertyInfo)
ElseIf GetType(ICollection).IsAssignableFrom(PropertyInfo.PropertyType) Then
Template = New ListTemplate(DataSource, PropertyInfo, Report.InputReportParameters(Array.IndexOf(Properties, PropertyInfo)).Enumeration.MultiSelect)
End If
If Template IsNot Nothing Then
Dim TemplateField As New TemplateField
TemplateField.HeaderText = PropertyInfo.Name
TemplateField.ItemTemplate = Template
DetailsView1.Fields.Add(TemplateField)
End If
Next
End Sub
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
CheckAuthentication()
BuildDetailView(Report.ReportParametersDataObject)
If Report.ReportParameters.Any AndAlso Not Report.InputReportParameters.Any Then
Run() 'no parameters are for data entry
ElseIf Not IsPostBack Then
lblReportType.Text = String.Format("Report {0}", Report.Type)
DetailsView1.DataSource = New List(Of Object) From {Report.ReportParametersDataObject}
DetailsView1.DataBind()
End If
End Sub
Protected Sub DetailsView1_ItemUpdating(sender As Object, e As System.Web.UI.WebControls.DetailsViewUpdateEventArgs) Handles DetailsView1.ItemUpdating
Run(e.NewValues)
End Sub
List Template helper:
Public Class ListTemplate
Implements System.Web.UI.ITemplate
Public Sub New(DataSource As Object, PropertyInfo As System.Reflection.PropertyInfo, Multiselect As Boolean)
Me.DataSource = DataSource
Me.PropertyInfo = PropertyInfo
Me.Multiselect = Multiselect
End Sub
Private DataSource As Object
Private Multiselect As Boolean
Private PropertyInfo As System.Reflection.PropertyInfo
Public Sub InstantiateIn(container As System.Web.UI.Control) Implements System.Web.UI.ITemplate.InstantiateIn
Dim ListBox As New System.Web.UI.WebControls.ListBox
ListBox.DataSource = PropertyInfo.GetValue(DataSource, Nothing)
ListBox.DataTextField = "Text"
ListBox.DataValueField = "Value"
ListBox.SelectionMode = IIf(Multiselect, ListSelectionMode.Multiple, ListSelectionMode.Single)
ListBox.Rows = Math.Min(25, ListBox.DataSource.count)
ListBox.EnableViewState = True
container.Controls.Add(ListBox)
End Sub
End Class
DateTemplate Helper:
Public Class DateTemplate
Implements System.Web.UI.ITemplate
Public Sub New(DataSource As Object, PropertyInfo As System.Reflection.PropertyInfo)
Me.DataSource = DataSource
Me.PropertyInfo = PropertyInfo
End Sub
Private DataSource As Object
Private PropertyInfo As System.Reflection.PropertyInfo
Public Sub InstantiateIn(container As System.Web.UI.Control) Implements System.Web.UI.ITemplate.InstantiateIn
Dim Textbox As New System.Web.UI.WebControls.TextBox
Textbox.ID = PropertyInfo.Name.Replace(" ", String.Empty)
Textbox.EnableViewState = True
Dim CalendarExtender As New AjaxControlToolkit.CalendarExtender
CalendarExtender.TargetControlID = Textbox.ID
CalendarExtender.SelectedDate = PropertyInfo.GetValue(DataSource, Nothing)
CalendarExtender.DefaultView = AjaxControlToolkit.CalendarDefaultView.Months
CalendarExtender.Format = "MMMM yyyy"
container.Controls.Add(Textbox)
container.Controls.Add(CalendarExtender)
End Sub
End Class
In the absense of any answers or even comments, I worked around this issue by creating my own asp.net custom control.
The way it works is very simple. It has a DataSource property and it creates the UI adding an edit widget for each property. According to the propertyinfo.propertytype, it creates an editor for that type.
Once the user submits changes it raises a server-side event handing back the datasource object containing the changed values.
It's so simple, even I can understand how to use it.
Dim zonename As String = DropDownList1.SelectedItem.Text
It always display the first value from the dropdownlist
try it binding in is not postback
Reference
If you bind inside page load you will always get the first value of the dropdown list.
private void Page_Load()
{
if (!IsPostBack)
{
//bind your dropdown here
}
}
In VB
Sub Page_Load
If Not IsPostBack
' bind your dropdown list
Validate()
End If
End Sub
Edit 1
Storing Connection string you can use web.config file
http://www.connectionstrings.com/Articles/Show/store-connection-string-in-web-config
use page.ispostback
within page load event...
if NOT page.isPostBack Then
Dim zonename As String = DropDownList1.SelectedItem.Text
End if
I have some code that handles a timer method, that just updates a label and changes it to a last updated at this time.
Protected Sub specialNotesTimer_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles specialNotesTimer.Tick
Label1.Text = "Panel refreshed at: " + DateTime.Now.ToLongTimeString()
End Sub
When it ticks however, any other control on the page cannot be found via a button press. Image buttons or my other dynamically created controls, w.e the case, are missing.
I have a function that finds the fired control on the page, then returns the control so I can determine what to do
Public Shared Function GetPostBackControl(ByVal thePage As Page) As Control
Dim myControl As Control = Nothing
Dim ctrlName As String = thePage.Request.Params.Get("__EVENTTARGET")
If ((ctrlName IsNot Nothing) And (ctrlName <> String.Empty)) Then
myControl = thePage.FindControl(ctrlName)
Else
For Each Item As String In thePage.Request.Form
Dim c As Control = thePage.FindControl(Item)
If (TypeOf (c) Is System.Web.UI.WebControls.Button) Then
myControl = c
End If
Next
End If
Return myControl
End Function
This works before the timer.tick, but not after. The control won't be listed as an Item in thePage.Request.Form, and it'll throw a null exception, when I know the control is there.
Classic scenario: Take user input, get a search-result and display it in pages to the user. I then need to display buttons for First, Next, Previous etc, and I maintain the users current page in viewstate. All is good, works fine.
Then I need to implement clickable page numbers, ie. 1-2-3-4-5-6 etc.
Rendering them is simple. I generate a linkbutton control at runtime, add commandargument with the page number and add a handler to it, so click are to be handled. Then I add it to a placeholder, and it is displayed as expected.
But then... If I did not already have a shaved head, I would be pulling out my hair getting the events to fire as expected every time.
How should I do this, so my events are always wired up and able to fire when the paging-linkbuttons are called?
Below is the important parts of the code, some pseudo to make it (hopefully) easier to understand, what I am doing.
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not Page.IsPostBack Then
Search()
End If
End Sub
Sub Search
'Misc databinding stuff, searches and displays results for the page specified in Me.CurrentPage
RenderPagingControls()
End Sub
Sub RenderPagingControls
'loop throug pagenumbers, Build a linkbutton control, add it to a placeholder
AddHandler lbn.Click, AddressOf lbnNumber_Click
lblPageNumbers.Controls.Add(lbn)
...
End Sub
Protected Sub lbnNumber_Click(ByVal sender As Object, ByVal e As EventArgs)
Dim b As LinkButton = CType(sender, LinkButton)
Me.CurrentPage = CInt(b.CommandArgument)
Search()
End Sub
Public Property CurrentPage() As Integer
Get
Dim o As Object = Me.ViewState("CurrentPage")
If o Is Nothing Then
Return 1
Else
Return CType(o, Integer)
End If
End Get
Set(ByVal value As Integer)
Me.ViewState("CurrentPage") = value
End Set
End Property
Protected Sub lbnNumber_Click(ByVal sender As Object, ByVal e As EventArgs)
Dim b As LinkButton = CType(sender, LinkButton)
Me.CurrentPage = CInt(b.CommandArgument)
Search()
End Sub
I'm going to recommend against a LinkButton and recommend Hyperlinks / QueryString parameters instead. For several reasons:
Your page will be much more efficient without the viewstate overhead of a link button.
If these are public facing pages, you'll get better indexing of all the pages if they can be accessed via hyperlinks (and indexed via search engines).
You'll find them much easier to implement. No event management, etc.
You would redefine your CurrentPage method as (hopefully this is correct, I'm better at C# than vb.net):
Public Property CurrentPage() As Integer
Get
Dim o As Object = Me.Request.QueryString("page")
If o Is Nothing Then
Return 1
Else
Return CType(o, Integer)
End If
End Get
End Property
Then just add hyperlinks for each page.
<a href='mypage.aspx?page=1'>1</a> - <a href='mypage.aspx?page=2'>2</a>
etc...
Alternative: If you want to use the LinkButton, you might want to consider putting a single LinkButton in a repeater. Then the only event you have to worry about is the OnItemCommand event. Then no dynamic controls or events. Something like this:
<asp:Repeater ID="rptPages" runat="server" OnItemCommand='doPaging'>
<ItemTemplate>
<asp:LinkButton ID="LinkButton1" runat="server" Text='<%# (Container.DataItem).ToString() %>'
CommandArgument='<%# (Container.DataItem).ToString() %>' />
</ItemTemplate>
<SeparatorTemplate>-</SeparatorTemplate>
</asp:Repeater>
Bind this control to an array (or list) of consecutive Integers (as many are there are pages). Then in your doPaging function (as I call it), check RepeaterCommandEventArgs.CommandArgument to get the page number.
Thanks for the answers, guys. I tried out Austins first, but I must be missing something, because I keep getting the same behavior of link buttons only working every second time... So I gave up on that, and saw the alternative solution with the repeater by Keltex! It is as brilliant as it is simple, and we don't have to worry about any page life-cycle bullshit.
It just really works! ;)
If somebody should need something similar in the future, here is the relevant code behind the scenes:
Sub Search()
...
RenderPagingControls()
End Sub
Sub RenderPagingControls()
Dim pages As New ArrayList
For i As Integer = 1 To Me.PageCount
pages.Add(i)
Next
repPageNumbersTop.DataSource = pages
repPageNumbersTop.DataBind()
repPageNumbersBottom.DataSource = pages
repPageNumbersBottom.DataBind()
End Sub
Public Property CurrentPage() As Integer
Get
Dim o As Object = Me.ViewState("CurrentPage")
If o Is Nothing Then
Return 1
Else
Return CType(o, Integer)
End If
End Get
Set(ByVal value As Integer)
Me.ViewState("CurrentPage") = value
End Set
End Property
Public Property PageCount() As Integer
Get
Dim o As Object = Me.ViewState("PageCount")
If o Is Nothing Then
Return 0
Else
Return CType(o, Integer)
End If
End Get
Set(ByVal value As Integer)
Me.ViewState("PageCount") = value
End Set
End Property
Protected Sub repPageNumbersTop_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles repPageNumbersTop.ItemCommand, repPageNumbersBottom.ItemCommand
Me.CurrentPage = CType(e.CommandArgument, Integer)
Search()
End Sub
Private Sub repPageNumbersTop_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.RepeaterItemEventArgs) Handles repPageNumbersTop.ItemDataBound, repPageNumbersBottom.ItemDataBound
If e.Item.ItemType = ListItemType.Item Or e.Item.ItemType = ListItemType.AlternatingItem Then
Dim lbn As LinkButton = CType(e.Item.FindControl("lbnPageNumber"), LinkButton)
If lbn.CommandArgument = Me.CurrentPage.ToString Then
lbn.Enabled = False
End If
End If
End Sub
This code works (sorry it's in C#):
protected void SearchButton_Click(object sender, EventArgs e)
{
//clear the collection!
pnlPageNumber.Controls.Clear();
//simulate search
System.Random rnd = new Random();
//create page buttons
for (int i = 0; i < rnd.Next(3, 15); i++)
{
LinkButton lb = new LinkButton();
pnlPageNumber.Controls.Add(lb);
lb.ID = "btn" + i;
lb.Text = i.ToString();
lb.CommandArgument = i.ToString();
lb.Command += new CommandEventHandler(linkbutton_Command);
//optional literal
pnlPageNumber.Controls.Add(new LiteralControl(" "));
}
ViewState["control#"] = Panel1.Controls.Count;
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
//Recreate link buttons
//This is necessary to ensure proper event binding
int count = 0;
if (ViewState["control#"] != null)
count = (int)ViewState["control#"];
for (int i = 0; i < count; i++)
{
LinkButton lb = new LinkButton();
pnlPageNumber.Controls.Add(lb);
lb.ID = "btn" + i; //make sure IDs are the same here and on Search
lb.Command += new CommandEventHandler(linkbutton_Command);
//this is not necessary, but if you do, make sure its in both places
pnlPageNumber.Controls.Add(new LiteralControl(" "));
}
}
}
void linkbutton_Command(object sender, CommandEventArgs e)
{
Response.Write(e.CommandArgument.ToString() + " CLICK<br />");
}
You could use the DataPager control -- the only limitation is you have to use it with the ListView control, but you should be able to represent your data using the ListView control fairly easily because it is very flexible. You can set the DataSource of the ListView control to the result of your data result, whether that be a DataSet, Collection, Array, etc.
To create the paging controls with "first", "last", and page numbers, set up the DataPager like this (where ListView1 is the ID of your ListView control):
<asp:DataPager ID="DataPager1" runat="server"
PagedControlID="ListView1" PageSize="25">
<Fields>
<asp:NextPreviousPagerField FirstPageText="first" ShowFirstPageButton="True"
ShowNextPageButton="False" ShowPreviousPageButton="False" />
<asp:NumericPagerField />
<asp:NextPreviousPagerField LastPageText="last" ShowLastPageButton="True"
ShowNextPageButton="False" ShowPreviousPageButton="False" />
</Fields>
</asp:DataPager>
By design, the DataPager uses the whole result set from the database, but you can improve the performance by caching the result and using that on the subsequent requests.
Hope this helps.
iirc... adding controls dynamically at runtime is a bit tricky. The control tree must be rebuilt during post back... but before viewstate is loaded (not sure when in the page life cycle... but way before page load). So... your problem is that by the time asp.net is trying to figure out your event the actual originating control has not yet been created.