Two Way Databinding Code Example (NorthWind)

Posts   
 
    
psandler
User
Posts: 540
Joined: 22-Feb-2005
# Posted on: 18-Jul-2006 23:20:18   

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:

  1. The data is fetched through an additional layer (so LivePersistence = False)
  2. Changes are stored in a unit of work and persisted all at once (not for each change)
  3. 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. simple_smile 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>
    
        &nbsp;
        <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>
        &nbsp;&nbsp;
        <b>Contact Name:</b> <asp:Label ID="ContactNameLabel" runat="server"></asp:Label>
        &nbsp;&nbsp;
        <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"/>
            &nbsp;&nbsp;&nbsp;
        <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
    }
}

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 19-Jul-2006 07:00:38   

Good effort. Thanks

ADF1969
User
Posts: 37
Joined: 28-Mar-2006
# Posted on: 25-Nov-2006 08:06:33   

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.

Chester
Support Team
Posts: 223
Joined: 15-Jul-2005
# Posted on: 26-Nov-2006 05:23:03   

See my reply to your other post. BTW It's better to start a new thread (like you did) than to tack on to old threads. No big deal but I thought I'd let you know since it makes responding to your posts easier.