- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
Two Way Databinding Code Example (NorthWind)
Joined: 22-Feb-2005
Ok, finally got some time to finish this (with help from Frans via http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=6855&HighLight=1).
What I wanted to do was work up a simple example of using 2-way databinding, where:
- The data is fetched through an additional layer (so LivePersistence = False)
- Changes are stored in a unit of work and persisted all at once (not for each change)
- It works without using any kind of client-side editing through a third-party grid.
Getting some parts to work was tricky--things were happening in the page lifecycle that I didn't understand. Part of getting it to work involved setting breakpoints and understanding WHAT happened WHEN.
As you can see from the final version, you can do a lot with just a little code. Note how minimal the actual data/databinding code is--the lion's share of the code revolves around managing the state of the page and handling events.
There is no validation of any kind on the page or the entities. For example, if you change the customerId, you may get an error.
One other note: deletes don't work in this example because of foreign key constraints. I left the buttons in to show how easy it is to handle this functionality, but if you try to delete a row, you will get an exception.
I have put my comments in the code. Suggestions for improvement (in my ASP.Net, LLBL, or .net code) are always welcome.
FetchUtility.cs
using SD.LLBLGen.Pro.ORMSupportClasses;
using Version2.DAL.DatabaseSpecific;
using Version2.DAL.EntityClasses;
using Version2.DAL;
namespace Version2.DAO
{
public static class FetchUtility
{
public static CustomersEntity FetchCustomer(string customerId)
{
PrefetchPath2 prePath = new PrefetchPath2((int)EntityType.CustomersEntity);
prePath.Add(CustomersEntity.PrefetchPathOrders);
CustomersEntity entity = new CustomersEntity(customerId);
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntity(entity, prePath);
}
return entity;
}
}
}
SaveUtility.cs
using SD.LLBLGen.Pro.ORMSupportClasses;
using Version2.DAL.DatabaseSpecific;
namespace Version2.DAO
{
public static class SaveUtility
{
public static void SaveUOW(UnitOfWork2 uow)
{
DataAccessAdapter adapter = new DataAccessAdapter();
uow.Commit(adapter, true);
}
}
}
TypedListUtility.cs
using System.Data;
using Version2.DAL.HelperClasses;
using Version2.DAL.DatabaseSpecific;
namespace Version2.DAO
{
public static class TypedListUtility
{
public static DataSet GetCustomerList()
{
ResultsetFields fields = new ResultsetFields(2);
fields.DefineField(CustomersFields.CustomerId, 0);
fields.DefineField(CustomersFields.CompanyName, 1);
DataAccessAdapter adapter = new DataAccessAdapter();
DataSet DS = new DataSet();
DataTable DT = new DataTable();
DS.Tables.Add(DT);
adapter.FetchTypedList(fields, DT, null);
return DS;
}
}
}
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Version2.WEB._Default" %>
<%@ Register Assembly="SD.LLBLGen.Pro.ORMSupportClasses.NET20" Namespace="SD.LLBLGen.Pro.ORMSupportClasses"
TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Untitled Page</title>
</head>
<body style="font-family:Arial">
<form id="form1" runat="server">
<div>
<cc1:LLBLGenProDataSource2
ID="ordersDS"
CacheLocation="Session"
runat="server"
OnPerformSelect="ordersDS_PerformSelect"
OnPerformWork="ordersDS_PerformWork"
AdapterTypeName="Version2.DAL.DatabaseSpecific.DataAccessAdapter, Version2.DALDBSpecific"
DataContainerType="EntityCollection"
EntityFactoryTypeName="Version2.DAL.FactoryClasses.OrdersEntityFactory, Version2.DAL"
LivePersistence="False">
</cc1:LLBLGenProDataSource2>
<asp:Label
ID="messageLabel"
runat="server"
Visible="false"
Font-Names="Arial"
Font-Bold="true"
ForeColor="red"
/>
<asp:Literal ID="br1" runat="server" Text="<br />"></asp:Literal>
<asp:Literal ID="br2" runat="server" Text="<br />"></asp:Literal>
<asp:Literal ID="hr1" runat="server" Text="<hr />"></asp:Literal>
<asp:DropDownList
ID="customerList"
runat="server"
OnSelectedIndexChanged="customerList_SelectedIndexChanged"
AutoPostBack="true"
/>
<br />
<br />
<b>Company Name:</b> <asp:Label ID="CompanyNameLabel" runat="server"></asp:Label>
<b>Contact Name:</b> <asp:Label ID="ContactNameLabel" runat="server"></asp:Label>
<b>Contact Title:</b> <asp:Label ID="ContactTitleLabel" runat="server"></asp:Label>
<br />
<br />
<b style="font-size:larger">Orders</b>
<asp:GridView
ID="ordersGrid"
runat="server"
DataKeyNames="OrderId"
DataSourceId="ordersDS"
OnRowCommand="ordersGrid_RowEdit"
CellPadding="1"
CellSpacing="2"
>
<Columns>
<asp:CommandField
ShowDeleteButton="true"
ShowCancelButton="true"
ShowEditButton="true"
/>
</Columns>
</asp:GridView>
<br />
<asp:Button
ID="saveButton"
runat="server"
Text="SAVE"
OnClick="btnSave_Click"/>
<asp:Button
ID="cancelButton"
runat="server"
Text="CANCEL"
OnClick="btnCancel_Click"/>
</div>
</form>
</body>
</html>
Default.aspx.cs
using System;
using SD.LLBLGen.Pro.ORMSupportClasses;
using Version2.DAL.EntityClasses;
using Version2.DAO;
using System.Collections.Generic;
using Version2.DAL.HelperClasses;
using Version2.DAL.FactoryClasses;
using System.Web.UI.WebControls;
namespace Version2.WEB
{
public partial class _Default : System.Web.UI.Page
{
#region "Global" Variables
/*
* Someone showed me this technique of using getters and setters to simulate global variables
* for a page using Session or ViewState. It's a simple trick that I find very useful.
*/
private string CustomerId
{
get { return (string)ViewState["CustomerId"]; }
set { ViewState["CustomerId"] = value; }
}
//Have to keep a separate version of Orders, since Customer.Orders is read-only
private EntityCollection<OrdersEntity> Orders
{
get { return (EntityCollection<OrdersEntity>)ViewState["Orders"]; }
set { ViewState["Orders"] = value; }
}
private string GUID
{
get
{
if (ViewState["GUID"] == null)
{
ViewState["GUID"] = Guid.NewGuid().ToString();
}
return (string)ViewState["GUID"];
}
}
private UnitOfWork2 UOW
{
get
{
if (ViewState[GUID + "-UOW"] == null)
{
ViewState[GUID + "-UOW"] = new UnitOfWork2();
}
return (UnitOfWork2)ViewState[GUID + "-UOW"];
}
}
#endregion
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
customerList.DataSource = TypedListUtility.GetCustomerList().Tables[0];
customerList.DataTextField = "CompanyName";
customerList.DataValueField = "CustomerId";
customerList.DataBind();
PopulateOrders(true);
}
ShowMessageControls(false);
}
protected void ordersGrid_RowEdit(object sender, GridViewCommandEventArgs e)
{
//hide/show buttons based on whether a row is being edited.
if (e.CommandName == "Edit")
{
saveButton.Visible = false;
cancelButton.Visible = false;
}
else
{
saveButton.Visible = true;
cancelButton.Visible = true;
}
}
private void ShowMessage(string message)
{
ShowMessageControls(true);
messageLabel.Text = message;
}
private void ShowMessageControls(bool show)
{
messageLabel.Visible = show;
br1.Visible = show;
br2.Visible = show;
hr1.Visible = show;
}
private void PopulateOrders(bool calledFromPageLoad)
{
CustomersEntity customer = FetchUtility.FetchCustomer(customerList.SelectedValue);
CompanyNameLabel.Text = customer.CompanyName;
ContactNameLabel.Text = customer.ContactName;
ContactTitleLabel.Text = customer.ContactTitle;
CustomerId = customer.CustomerId;
Orders = (EntityCollection<OrdersEntity>)customer.Orders;
if (!calledFromPageLoad)
{
/* I could not find a workaround for this. If you set this when called from page_load,
* the entityCollection somehow gets re-instantiated (count=0) when it gets to PerformSelect.
*
* I think before PerformSelect happens, another event clears the EntityCollection (ExecuteSelect?)
*
* If you don't call it at all, the newly fetched data doesn't get set as the datasource for
* the grid.
*
* I think that you simply have to take this step any time PerformSelect isn't going to be
* called. Since it IS called as part of page_load, you need to skip over the manual
* resetting of the entitycollection and the databinding.
*/
ordersDS.EntityCollection = Orders;
ordersGrid.DataBind();
}
}
protected void customerList_SelectedIndexChanged(object sender, EventArgs e)
{
if (HasUncommited(UOW))
{
ShowMessage("You have uncommitted changes. Click Save or Cancel before changing the customer.");
//set the list back to its pre-changed state
customerList.SelectedValue = CustomerId;
}
else
{
PopulateOrders(false);
}
}
protected void ordersDS_PerformSelect(object sender, PerformSelectEventArgs2 e)
{
//set the entitycollection of the datasource to the orders collection stored in ViewState
ordersDS.EntityCollection = Orders;
}
protected void ordersDS_PerformWork(object sender, PerformWorkEventArgs2 e)
{
//set the orders collection stored in ViewState to the entity collection
//of the datasource (to maintain changes on the screen)
Orders = (EntityCollection<OrdersEntity>)ordersDS.EntityCollection;
//move the changes of the DataSource's UOW to our "main" UOW
TransferUnitOfWorkElements(e.Uow, UOW);
//Remove a deleted entity (if any) from the collection by examining the UOW
RemoveUowEntityFromCollection(Orders, e.Uow);
/*
* I believe this is related to the note in method PopulateOrders. Underneath the hood,
* the EntityCollection automatically gets reset (to empty), so if we don't take this step,
* the Orders property will be empty when we hit PerformSelect
*/
//set the datasource entitycollection to a new entitycollection
ordersDS.EntityCollection = CreateNewEntityCollectionFromDataSource(sender);
}
protected void btnSave_Click(object sender, EventArgs e)
{
//Commit the UOW, Reset the UOW
SaveUtility.SaveUOW(UOW);
UOW.Reset();
PopulateOrders(false);
ShowMessage("Save Complete.");
}
protected void btnCancel_Click(object sender, EventArgs e)
{
//reset the UOW
UOW.Reset();
PopulateOrders(false);
}
#region Move these to a Utility Class
private static bool HasUncommited(UnitOfWork2 uow)
{
//test if UOW has uncommitted changes--there may be an easier way to do this.
//In this particular case, I could probably just test for dirty
int unCommittedCount = 0;
bool returnValue;
List<UnitOfWorkCollectionElement2> collectionElements;
List<UnitOfWorkElement2> entityElements;
collectionElements = uow.GetCollectionElementsToDelete();
unCommittedCount += collectionElements.Count;
collectionElements = uow.GetCollectionElementsToSave();
unCommittedCount += collectionElements.Count;
entityElements = uow.GetEntityElementsToInsert();
unCommittedCount += entityElements.Count;
entityElements = uow.GetEntityElementsToUpdate();
unCommittedCount += entityElements.Count;
entityElements = uow.GetEntityElementsToDelete();
unCommittedCount += entityElements.Count;
if (unCommittedCount == 0)
{
returnValue = false;
}
else
{
returnValue = true;
}
return returnValue;
}
public static void RemoveUowEntityFromCollection(IEntityCollection2 ec, UnitOfWork2 uow)
{
//in order to remove an element from the collection, we need to examine the UOW
List<UnitOfWorkElement2> entityElements = uow.GetEntityElementsToDelete();
foreach (UnitOfWorkElement2 element in entityElements)
{
ec.Remove((EntityBase2)element.Entity);
}
}
public static EntityCollection CreateNewEntityCollectionFromDataSource(object senderDataSource2)
{
//this is a little ham-handed, but it eliminates the possibility of choosing the wrong factory (or having to choose the factory at all)
return new EntityCollection(((LLBLGenProDataSource2)senderDataSource2).EntityCollection.EntityFactoryToUse);
}
//transfer the contents of one UOW to another.
//Is there a better/more efficient way to do this?
public static void TransferUnitOfWorkElements(UnitOfWork2 source, UnitOfWork2 destination)
{
List<UnitOfWorkCollectionElement2> collectionElements;
List<UnitOfWorkElement2> entityElements;
//transfer collectionElementsToDelete
collectionElements = source.GetCollectionElementsToDelete();
for (int x = 0; x < collectionElements.Count; x++)
{
destination.AddCollectionForDelete(collectionElements[x].Collection);
}
//transfer collectionElementsToSave
collectionElements = source.GetCollectionElementsToSave();
for (int x = 0; x < collectionElements.Count; x++)
{
destination.AddCollectionForSave(collectionElements[x].Collection);
}
//transfer entities to insert
entityElements = source.GetEntityElementsToInsert();
for (int x = 0; x < entityElements.Count; x++)
{
destination.AddForSave(entityElements[x].Entity);
}
//transfer entities to update
entityElements = source.GetEntityElementsToUpdate();
for (int x = 0; x < entityElements.Count; x++)
{
destination.AddForSave(entityElements[x].Entity);
}
//transfer entities to delete
entityElements = source.GetEntityElementsToDelete();
for (int x = 0; x < entityElements.Count; x++)
{
destination.AddForDelete(entityElements[x].Entity);
}
}
#endregion
}
}
Joined: 28-Mar-2006
This line
return new EntityCollection(((LLBLGenProDataSource2)senderDataSource2).EntityCollection.EntityFactoryToUse);
doesn't work for me. I don't see a "EntityCollection" type that can be instantiated - I seem to recall that that got removed for v2.0.
So what is the equivalent line for v2.0?
Also - why is that necessary?
I see your comments, and I have tested this in my code as well, and in the Beginning of the PerformWork my "DataSource.EntityCollection" has items in it - but then later, it gets 0'd out...what is up with that?
Thanks,
Andrew.