Two-way data binding in ASP.NET - asp.net

Trying to use data binding between a list of objects and a data list control. What I want to do are
create the list of objects
have them bound to the controls
change data in the UI
have the changes in the ui bound to the list of objects
on post back - have the list of objects with the new values from the ui
<body>
<form id="form1" runat="server">
<div>
<asp:DataList ID="DataList1" runat="server" DataKeyField="ClassID" ViewStateMode="Enabled">
<ItemTemplate>
<asp:TextBox ID="txtValue1" runat="server" Text='<%# Bind("Value1") %>'></asp:TextBox>
<asp:TextBox ID="txtValue2" runat="server" Text='<%# Bind("Value2") %>'></asp:TextBox>
<asp:TextBox ID="txtvalue3" runat="server" Text='<%# Bind("Value3") %>'></asp:TextBox>
</ItemTemplate>
</asp:DataList>
<asp:Button ID="btnDoPostBack" runat="server" Text="Do Post Back" />
</div>
</form>
</body>
Option Explicit On
Option Strict On
Imports System.Diagnostics
Partial Class _Default
Inherits System.Web.UI.Page
Dim Class1List As List(Of Class1)
Protected Sub Page_PreLoad(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreLoad
Dim txtValue1 As TextBox
Dim txtValue2 As TextBox
Dim txtValue3 As TextBox
Dim ItemIndex As Integer = 0
If Page.IsPostBack Then
Class1List = CType(Session("Class1List"), List(Of Global.Class1))
'Class1List = CType(DataList1.DataSource, List(Of Global.Class1))
For Each myDataListItem As DataListItem In DataList1.Items
txtValue1 = CType(myDataListItem.FindControl("txtValue1"), TextBox)
Long.TryParse(txtValue1.Text, Class1List(ItemIndex).Value1)
txtValue2 = CType(myDataListItem.FindControl("txtValue2"), TextBox)
Integer.TryParse(txtValue2.Text, Class1List(ItemIndex).Value2)
txtValue3 = CType(myDataListItem.FindControl("txtValue3"), TextBox)
Class1List(ItemIndex).Value3 = txtValue3.Text
ItemIndex += 1
Next
End If
End Sub
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim myClass1 As Class1
If Not Page.IsPostBack Then
Class1List = New List(Of Class1)
myClass1 = New Class1
Class1List.Add(myClass1)
BindData()
Else
'Class1List = CType(DataList1.DataSource, List(Of Global.Class1))
Debug.WriteLine("Page_Load, Value1 = " & Class1List(0).Value1.ToString())
Debug.WriteLine("Page_Load, Value2 = " & Class1List(0).Value2.ToString())
Debug.WriteLine("Page_Load, Value3 = " & Class1List(0).Value3)
End If
End Sub
Protected Sub Page_Unload(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Unload
Session("Class1List") = Class1List
End Sub
Sub BindData()
DataList1.DataSource = Class1List
DataList1.DataBind()
End Sub
Protected Sub DataList1_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DataListItemEventArgs) Handles DataList1.ItemDataBound
Dim myClass1 As Class1
If e.Item.ItemType = ListItemType.Item OrElse e.Item.ItemType = ListItemType.AlternatingItem Then
myClass1 = CType(e.Item.DataItem, Class1)
Debug.WriteLine("DataList1_ItemDataBound, Value1 = " & myClass1.Value1.ToString())
Debug.WriteLine("DataList1_ItemDataBound, Value2 = " & myClass1.Value2.ToString())
Debug.WriteLine("DataList1_ItemDataBound, Value3 = " & myClass1.Value3)
End If
End Sub
Protected Sub btnDoPostBack_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnDoPostBack.Click
Dim myRandom As New Random
Class1List(0).Value1 = myRandom.Next(100)
Class1List(0).Value2 = myRandom.Next(100)
Class1List(0).Value3 = myRandom.Next(100).ToString()
Debug.WriteLine("btnDoPostBack_Click, Value1 = " & Class1List(0).Value1.ToString())
Debug.WriteLine("btnDoPostBack_Click, Value2 = " & Class1List(0).Value2.ToString())
Debug.WriteLine("btnDoPostBack_Click, Value3 = " & Class1List(0).Value3)
BindData()
End Sub
End Class
The Class Class1 is trivial:
Option Explicit On
Option Strict On
Imports Microsoft.VisualBasic
Public Class Class1
Private _ClassID As Long
Private _Value1 As Long
Private _Value2 As Integer
Private _value3 As String = String.Empty
Public Property ClassID As Long
Get
Return _ClassID
End Get
Set(ByVal value As Long)
_ClassID = value
End Set
End Property
Public Property Value1 As Long
Get
Return _Value1
End Get
Set(ByVal value As Long)
_Value1 = value
End Set
End Property
Public Property Value2 As Integer
Get
Return _Value2
End Get
Set(ByVal value As Integer)
_Value2 = value
End Set
End Property
Public Property Value3 As String
Get
Return _value3
End Get
Set(ByVal value As String)
_value3 = value
End Set
End Property
End Class
Update: I got the code behind above to do what I want it to do - I was thinking there was a better way?

You didn't show your databinding "Load" phase (the code which binds the data from the list to the controls)--so I assume the part you are unhappy with is the "Save" phase (the code in Page_PreLoad which binds the modified values from the controls back to the list), i.e. #4 in your list:
have the changes in the ui bound to the list of objects
It sounds like you want "two-way Data Binding": you want .NET to update your model as easily as it reads from your model. This is a common complaint. One solution is to subclass WebControl, but that's a mess.
You are already using the <%# Bind("...") %> syntax, so you have the right idea. That approach should work out-of-the-box with <asp:SqlDataSource>, but you want to update a custom class, so you need to use <asp:ObjectDataSource> instead. Use the approach in this article, except with ObjectDataSource instead of SqlDataSource.
But first you have to make your model (i.e., Class1) compatible with ObjectDataSource by marking it with [System.ComponentModel.DataObject] and designating the appropriate update method like this:
[System.ComponentModel.DataObjectMethodAttribute(
System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(string productName, ...) {
...
}
This would allow you to use an ObjectDataSource on your webform and finally get nice 2-way databinding. Read the links to for full details.
Visual Studio offers various clunky ways of automating this, such as TableAdapters and the infamous Strongly-Typed DataSet (STD), but those don't help people like yourself who have their own object model. I don't recommend STDs anyway.
I was thinking there was a better way?
I don't think your current approach is bad. If you're worried about having tons of logic in your webforms, you would gain much more by using an MVC approach than worrying about binding sugar...

Related

Writing a passed Array via session into Label isn't working

I am trying to take an array of string (Filled from a listbox on a previous page and passed via Session) and display it in a label,this is how i got the array:
Protected Sub Button1_Click(sender As Object, e As EventArgs) Handles CheckOut.Click
Dim x = ListBox1.GetSelectedIndices.Count
Dim ListPNames(x) As String
Dim i As Integer
i = 0
For Each item As String In ListBox1.GetSelectedIndices
ListPNames(i) = (ListBox1.SelectedItem).ToString
i = i + 1
Next
Session("SlctdPhones") = ListPNames(x)
Response.Redirect("CheckOut.aspx")
End Sub
And this is how i am trying to display it :
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim SlctdPhones() As String = CType(Session.Item("SlctdPhones"), Array)
Dim i As Integer
Label3.Text = ""
For i = 0 To SlctdPhones.Length - 1
Label3.Text += SlctdPhones(i).ToString() + Environment.NewLine
Next
End Sub
It is giving me an error :Object reference not set to an instance of an object. when it reaches the SlctdPhones.Length - 1 Line!!
i don't know how i can fix it ,also is my array code correct(Is everything being stored correctly in it?)
You declare the For loop like this:
For Each item In ...
But then never use the item variable in the body of the loop. Instead, you keep using the same SelectedItem property. You want to change that whole method to look like this:
Protected Sub Button1_Click(sender As Object, e As EventArgs) Handles CheckOut.Click
Dim PNames As New List(Of String)()
For Each index As Integer In ListBox1.GetSelectedIndices
PNames.Add(ListBox1.Items(index).Value)
Next
Session("SlctdPhones") = PNames
Response.Redirect("CheckOut.aspx")
End Sub
With that fixed, the Page_Load can do this:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim SlctdPhones As List(Of String) = TryCast(Session.Item("SlctdPhones"), List(Of String))
If SlctdPhones Is Nothing OrElse SlctdPhones.Length = 0 Then
'Something went wrong here!
Return
End If
Label3.Text = String.Join("<br/>", SlctdPhones.ToArray())
End Sub
But I'd really love to see you use a data control rather than stuffing <br/>s into a label. Here's markup for a ListView:
<asp:ListView ID="ListView1" runat="server">
<LayoutTemplate>
<ul>
<asp:PlaceHolder ID="itemPlaceholder" runat="server" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li><%# Container.DataItem.ToString() %></li>
</ItemTemplate>
<EmptyDataTemplate>
<p>Nothing here.</p>
</EmptyDataTemplate>
</asp:ListView>
And then Page_Load is even simpler:
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
ListView1.DataSource = Session.Item("SlctdPhones")
ListView1.DataBind()
End Sub
On display page, use Literal instead of Label
Dim SlctdPhones() As String = CType(Session.Item("SlctdPhones"), Array)
Dim result as String = string.Join("<br>", SlctdPhones) 'Instead of <br> try Environment.NewLine as well
YourLitetal = result
Hope this helps!

DetailsView Trouble Retrieving Values

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.

Using DropDownList in FromView control

I'm trying to get a drop down list control to work in FormView. I need to have the list be a filtered view of a certain table and still be bound to a field in the data I'm editing. I've tried setting the item data programatically, ant that works but then the data binding
doesn't work, It tries to insert null into the database.
This is the code I've tried. I've also tried doing the same thing in several other events, it still tries to insert null into the database.
<asp:DropDownList ID="lstManagers" runat="server"
OnDataBound ="ManagersLoad"
SelectedValue='<%# Bind("UserName") %>' Width="100%"
DataSourceID="TimeOff" DataTextField="UserName" DataValueField="UserName">
</asp:DropDownList>
Protected Sub ManagersLoad(ByVal sender As Object, ByVal e As System.EventArgs)
Dim lst As DropDownList = FormView1.FindControl("lstManagers")
'get list of managers
Using ef As New TimeOffData.TimeOffEntities
For Each item As ListItem In lst.Items
Dim li As ListItem = item
item.Text = (From x In ef.TimeOffUsers Where x.UserName = li.Value Select x.FirstName & " " & x.LastName).FirstOrDefault
Next
End Using
End Sub
I took all the data binding stuff of the control and just decide it would be easier to do it manually. I've changed the code to this,
Protected Sub FormView1_ItemInserting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.FormViewInsertEventArgs) Handles FormView1.ItemInserting
Dim lst As DropDownList = FormView1.FindControl("lstManagers")
e.Values.Item("ManagerName") = lst.SelectedValue
End Sub
Protected Sub ManagersLoad(ByVal sender As Object, ByVal e As System.EventArgs)
Dim lst As DropDownList = FormView1.FindControl("lstManagers")
'get list of managers
Using ef As New TimeOffData.TimeOffEntities
Dim mng = From x In ef.TimeOffUsers Where x.IsManager = True
For Each item In mng
lst.Items.Add(New ListItem(item.FirstName & " " & item.LastName, item.UserName))
Next
End Using
End Sub

Pass GridView Cell Value to Sub Routine in ASP.NET / VB.NET

I have a GridView with Cell 0 containing the ID that I need to pass to a Public Sub.
I cannot figure out how to pick the value from Cell 0 in order to pass it to the Sub. I have tried experimenting (see the Dimmed EventID below) but have failed. Here is my code:
Protected Sub gvAppointmentsCalls_RowCommand(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewCommandEventArgs) Handles gvAppointmentsCalls.RowCommand
Dim EventID As String = gvAppointmentsCalls.Rows(e.RowIndex).Cells(0).Text
If e.CommandName = "gvAdd2Outlook" Then
Send_iCal_Call(EventID)
End If
End Sub
If I type the value directly e.g. Send_iCal_Call(123) then it works perfectly.
Public Sub Send_iCal_Call(ByRef Event_ID As Integer)
' My code in here
End Sub
Use the CommandArgument to pass the ID to the RowCommand-Handler.
For example:
CommandName="gvAdd2Outlook" CommandArgument='<%# Bind("EventID")%>'
In my opinion you should obtain your ID value within GridView's DataKeyNames property. You should define it in your grid markup this property like here
<asp:GridView DataKeyNames="EvenID">
and then will access it in code behind:
Protected Sub GridView1_RowCommand(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewCommandEventArgs) Handles GridView1.RowCommand
If e.CommandName = "gvAdd2Outlook" Then
Dim EventIDString As String = GridView1.DataKeys.Item(e.RowIndex).Value.ToString()
Dim EventID As Integer
If Integer.TryParse(EventIDString, EventID) = False Then Throw New ArgumentException("Wrong EventID=" & EventID)
Send_iCal_Call(EventID)
End If
End Sub

Gridview binding with combobox's in header

Could anyone please enlighten me about how one might go about binding to a gridview in ASP.Net 4 in a scenario where the first row of my gridview should be the headers, the second should be a combobox for each column and the third is the beginning of my actual datasource.
If you can imagine what I am trying to achieve is an ability to create a binding between each column in the datagrid and another datasource. This binding is created by the user selecting a value in the comboboxes. However no matter what I try I cant seem to achieve this.
HeaderText1 | HeaderText2 | HeaderText3
ComboBox1 | ComboBox2 | ComboBox3
DataRow1 | DataRow1 | DataRow1
DataRow2 | DataRow2 | DataRow2
DataRow3 | DataRow3 | DataRow3
You can put a DropDownList into a Gridview column quite easily by using a TemplateColumn:
<asp:GridView runat="server" ID="ComboboxGridView">
<Columns>
<asp:TemplateField HeaderText="Column 1">
<HeaderTemplate>
<asp:DropDownList runat="server" ID="Column1DropDownList" />
</HeaderTemplate>
<ItemTemplate>
<asp:Label runat="server" ID="Column1DisplayLabel" Text='<%# Eval("Column1") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
and you can bind the DropDownList to another data source quite easily, especially you're using the DataSource controls. I'm not clear on what you're doing with the DropDownLists in the header though - is it for filtering the rows that appear in the GridView?
So for anyone curious this appears to be the solution to the problem.
Private Sub grdMainGrid_RowCreated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles grdMainGrid.RowCreated
If e.Row.RowType = DataControlRowType.Header Then
For Each itm As TableCell In e.Row.Cells
itm.Text = GenerateHeaderHTML()
Next
End If
End Sub
PS: If anyone has any better solutions I would love to hear them :-)
The following is the code I have in the GenerateHeaderHTML(). My code is a very specific case (and prob far from great). However note that you can use any html you wish.
Private Sub grdMainGrid_RowCreated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles grdMainGrid.RowCreated
If Me.BoundedObjects IsNot Nothing Then
If e.Row.RowType = DataControlRowType.Header Then
Dim PrimitivePropertyNames As List(Of String) = ParserHelper.GetPrimitivePropertyNames(Me.BoundedObjects.ToList)
Dim i As Integer = 0
For Each itm As TableCell In e.Row.Cells
itm.Text = ucStockImport.CreateBindingHeaderTable(itm.Text, PrimitivePropertyNames, i.ToString)
i += 1
Next
End If
Else
Throw New StockImportException("ucStockImport.BoundedObjects Is Nothing")
End If
End Sub
Private Shared Function CreateBindingHeaderTable(ByVal HeaderText As String, ByVal PropertyNames As List(Of String), ByVal ID As String) As String
Return String.Format("<table><tr><td>{0}</td></tr><tr><td>{1}</td></tr></table>", HeaderText, ucStockImport.CreateBindedObjectDropDownList(PropertyNames, ID))
End Function
Private Shared Function CreateBindedObjectDropDownList(ByVal PropertyNames As List(Of String), ByVal ID As String) As String
Dim strBuilder As New StringBuilder
strBuilder.Append(String.Format("<option value=""{0}"">{1}</option>", i, propName))
Dim i As Integer = 0
For Each propName As String In PropertyNames
strBuilder.Append(String.Format("<option value=""{0}"">", i) & propName & "</option>")
i += 1
Next
strBuilder.Append("</select>")
Return strBuilder.ToString
End Function

Resources