Export (0) Print
Expand All
10 out of 12 rated this helpful - Rate this topic

Using the Entity Framework in n-Tier ASP.NET Applications

This chapter is excerpted from Programming Entity Framework: Building Data Centric Apps with the ADO.NET Entity Framework by Julia Lerman, published by O'Reilly Media

The preceding chapter focused on using the Entity Framework in an n-tier client-side application, as well as some of the UI challenges that you should be aware of. One of the key factors is that you were able to leverage a long-lived ObjectContext whose entities remain attached as they move into the UI and back. This allowed you to take advantage of Object Services change tracking.

Web applications are a completely different ballgame posing a new set of challenges for tracking changes in entities. Here, the client is a web page and the short-lived life cycle of a web page prevents entities from remaining attached to an ObjectContext. Additionally, because of the nature of server-based applications, any objects in memory are maintained on the server. For the developer, this means the server is responsible for maintaining objects for every user that is accessing the website at a given time, which could add up very quickly.

The biggest challenge of removing the UI's direct dependency on the Entity Framework in web applications is to figure out how to provide the state information that SaveChanges needs for updates in a way that balances the load between the client machine of the person accessing your site and the server. At the same time, you want to try to avoid unnecessary round trips to the database.

The EntityDataSource control goes a long way toward solving these problems; however, the EntityDataSource lives completely in the UI and precludes the separation of logic into layers.

In this chapter, we'll first look at the challenges and options available for handling entities in web applications, and then build a new set of classes whose job is to work with the ObjectContext and that can be accessed by the ASP.NET ObjectDataSource control. Finally, you will build a web page that relies on ObjectDataSource controls that are wired up to the new classes. In building this page, you will get an opportunity to address, learn about, and solve a number of issues that are specific to the fact that you are working with entities.

Understanding How an ObjectContext Fits into the Web Page Life Cycle

It will be helpful first to understand the life cycle of an ASP.NET web page in order to grasp why it creates a problem for the ObjectContext.

The Page object itself exists only for as long as it takes ASP.NET to render the HTML. Once the HTML has been created, the Page object is disposed along with any objects that it contained (see Figure 21.1, "ASP.NET page life cycle").

Figure 21.1. ASP.NET page life cycle

ASP.NET page life cycle

Even if you did all of your work with the ObjectContext in the page's code-behind, that context, which the page instantiates, will be destroyed when the page is disposed. Any entities that were created in the page will be destroyed as well. For this reason, web pages are stateless by nature (see Figure 21.2, "ASP.NET page using an ObjectContext").

Figure 21.2. ASP.NET page using an ObjectContext

ASP.NET page using an ObjectContext

Introducing a new layer into the mix continues to pose the problem of the disappearing context. Consider a class similar to the DataBridge class from Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications that instantiates an ObjectContext and retrieves or updates data on behalf of the UI using it.

Now the web page's .NET code can instantiate a new DataBridge class, which creates an ObjectContext and some entities. When the page completes its life cycle, it is disposed along with any objects that it owns, including the DataBridge object. When that object is disposed, so is the ObjectContext that it owns, and finally, the entities are also destroyed (see Figure 21.3, "Moving the ObjectContext out of the ASP.NET page and into a DataBridge class in the middle tier").

Figure 21.3. Moving the ObjectContext out of the ASP.NET page and into a DataBridge class in the middle tier

Moving the ObjectContext out of the ASP.NET page and into a DataBridge class in the middle tier

All of this means that you can't merely instantiate an ObjectContext, query for entities, modify those entities, and call SaveChanges. You will never be coming back to the same ObjectContext, and therefore change tracking won't be performed.

Using EntityObjects in Read-Only Web Pages

The big challenges for working with objects in ASP.NET occur mostly in scenarios where you need the user to update, especially with a graph of entities, something such as a master/detail page.

If you are building pages that merely need to display data, things are much simpler. You can return data of any shape to the page and use the page's code-behind to populate controls or bind data.

The most important thing to be aware of is that you should not return queries from the DataBridge class, but rather the result of those queries, whether that is a single object, a collection, a List and IEnumerable, or something else. Just do not return an ObjectQuery or a LINQ to Entities query. This is not because of the page life cycle, but because it's very easy to create a business object whose ObjectContext will be disposed before the query has actually executed. See the sidebar for a more in-depth explanation of this.

Return Results, Not Queries, from the DataBridge Class

Although you can get away with binding a query when working directly in the code-behind of an ASP.NET page, remember that the query's job is to be executed and return results. Query execution requires an ObjectContext. If you return the query itself from a business class, it will be detached from the context as soon as the business object is disposed (which in turn disposes the context).

Here's an example of a method within a business class that returns an IQueryable of customer objects:

public IQueryable<Customer> GetCustomer(int custID)
{
  return _ctx.Customer.Where(c => c.CustomerID == custID);
}

In a web app, you might set the data source of a control to the results. Because you are returning an IEnumerable, this will be allowed even though it contains only a single item:

ListView1.DataSource=dal.GetCustomer(570);

The query will not be executed until the page begins to render the ListView control. By then it's quite possible that the business object will be long gone and the execution will fail.

So, in the business class, be sure to return results, not queries. That way, you don't have to worry about how the methods are used from the UI.

An additional benefit is that by executing the query and forcing the results to be iterated through using something such as First, ToList, or Execute, when the iteration is complete the EntityConnection and its database connection are disposed. Therefore, you won't have to think twice about the database connection, which is an unmanaged resource.

You could have a method that returns a full graph of information for a customer along with her reservation information, payments for those reservations, and details regarding the trips for which she made the reservations. You can grab all of this information in a single query, as shown in Example 21.1, "Creating a deep graph to return to a web page for display".

Example 21.1. Creating a deep graph to return to a web page for display

Public Function GetCustomer(ByVal ContactID As Int32) As Customer
 Dim custs = _commonContext.Contacts.OfType(Of Customer) _
                           .Include("Addresses") _
                           .Include("Reservations.Trip.Location") _
                           .Include("PrimaryActivity") _
                           .Include("SecondaryActivity") _
                           .Include("PrimaryDestination") _
                           .Include("SecondaryDestination") _
                           .Where("it.Contactid=" & ContactID)

   Dim cust as Customer = custs.FirstOrDefault()
  'important return the customer object, not the query
   Return cust
End Function
public Customer GetCustomer(Int32 ContactID)
{
  var custs = _commonContext.Contacts.OfType<Customer>()
                            .Include("Addresses")
                            .Include("Reservations.Trip.Destination")
                            .Include("PrimaryActivity")
                            .Include("SecondaryActivity")
                            .Include("PrimaryDestination")
                            .Include("SecondaryDestination")
                            .Where("it.Contactid=" + ContactID);
   Customer cust = custs.FirstOrDefault();
 //important return the customer object, not the custs query;
   return cust;
}
Note
Remember that the downside to a query with so many Includes is that you might faint if you look at the T-SQL. The Entity SQL generated by this ObjectQuery will at least be cached and does not need to be compiled again during the application's lifetime if you need to run the same query. You could also rewrite the query as a LINQ expression and create a compiled query to invoke that instead. That won't reduce the complexity of the native query, but it will remove the time it creates to compile the LINQ query into the native query.

A web page could then instantiate the class, call GetCustomer, and then populate controls using the returned data, as shown in Example 21.2, "Retrieving entities from a separate class".

Example 21.2. Retrieving entities from a separate class

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles Me.Load

  If Not IsPostBack Then
    Dim bal = New DataBridge
    Dim cust = dal.GetCustomerwithRelatedData(_custID)
    persistKeyInfo(cust)
    populateTextBoxes(cust)
    gridView_Addresses.DataSource = cust.Addresses
    gridView_Addresses.DataBind()
    gridView_Reservations.DataSource = cust.Reservations
    gridView_Reservations.DataBind()
  End If
End Sub
protected void Page_Load(object sender, System.EventArgs e)
{
  if (!IsPostBack)
  {
    var bal = new DataBridge();
    var cust = dal.GetCustomerwithRelatedData(_custID);
    persistKeyInfo(cust);
    populateTextBoxes(cust);
    gridView _Addresses.DataSource = cust.Addresses;
    gridView _Addresses.DataBind();
    gridView _Reservations.DataSource = cust.Reservations;
    gridView _Reservations.DataBind();
  }
}

Because the default behavior of the controls is to save their display values in View State, it is not necessary to retrieve the data each time the page posts back.

A class similar to the one you created for the Windows Forms application that provides methods for retrieving data can do the trick. The web page can instantiate that class, request data, and then dispose the class.

As long as the user will not be editing any data, things are pretty straightforward.

Exploring Options for Updating Entities in an ASP.NET Application

The need to update data is where the complexities lie-especially if you want to use ObjectContext to track and save changes.

Single or batch updates?

It would be pragmatic to first determine whether you need to be able to update one entity (or graph) at a time, or more than one. This can make a big difference in how you approach the updates.

In web applications, it is common for a user to work with one object at a time and perform a save to the database before moving on to another object. This is the simplest scenario to implement and all of the ASP.NET data source controls use it, including EntityDataSource.

In some applications, having batch edits and update scenarios is desired. An example of this is editing a number of rows in a grid, and then performing an update when the user has completed all of his edits. This also introduces more potential concurrency conflicts if a user is holding onto modifications for a longer period of time. Implementing this scenario has always been a challenge, and the problems regarding persisting large amounts of data to enable batch updates in ASP.NET are not new. The Entity Framework merely adds a few more irons to the fire.

Persist entities or use independent values?

It's easiest to make a call to SaveChanges when you have a long-running ObjectContext that is not an option in the web scenario.

Note
What about Global.asax? Although it is technically possible to spin up an ObjectContext in Global.asax when the web application starts up on the server and to use that as a global cache for entities, this would wreak havoc on your web server. That single context will attempt to coordinate every user's queries and updates.

Without the long-running context, there are two possible paths to take.

The first involves persisting the entities in memory on the server-most likely in the user's session. When the user wants to update you can attach those entities to a new context and update them using the new values coming from the controls on the page, then call SaveChanges. As you learned in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications, you would have to detach each entity from the context and use binary serialization to store them in a variable. Although this avoids an extra round trip to the server, there are two downsides to watch out for that have to do with scalability as your user base grows, with more users hitting your website simultaneously. You'll read about these in the next few pages.

A potential compromise to storing the full graph is to store only the entity values, but you would need to store the relationships as well, and since the entities don't have much more information in them than their values, you won't reduce the amount of data that is being persisted. At the same time, walking through a graph and extracting the properties, as well as those of all of the related entities, and then rebuilding them will be quite an intensive process.

The second path for allowing SaveChanges to do its job would entail performing the query again prior to saving. You can then update the newly queried entities with the values coming from the client. You saw this pattern when writing web services and WCF services in Chapter 14, Using Entities with Web and WCF Services.

Before you consider whether to persist the data in memory, you will need to understand ASP.NET's mechanisms for storing data in memory and how those mechanisms are impacted when using entities.

Evaluating ASP.NET's State Solutions Against the Entity Framework

ASP.NET provides a number of mechanisms for maintaining the state of objects even as the Page object is destroyed. Let's take a look at those and see how they work for the entities.

View State

View State creates an encrypted binary stream of data representing objects or other data that it adds to the HTML of the page. Many ASP.NET controls use View State as a way to retain the contents of the control even if the page is posted back. For example, the text in a TextBox or the values in a DataGrid can automatically be stored in View State. When the page posts back, ASP.NET reads the View State and uses what it finds to help render the new HTML.

View State is a user interface mechanism and not something you would use outside of the ASP.NET Page class. If you don't have a lot of experience with ASP.NET, there are things about View State with respect to the Entity Framework that you should be aware of.

You can actually see the View State data if you view the source of an ASP.NET page in your web browser. The contents of View State can easily bloat the HTML and create performance problems if you use it without care. The biggest abuser of View State is generally a GridView, which could contain many rows and many columns' worth of data that it is trying to save across postbacks.

Although many of ASP.NET's controls and features can automatically read and write to View State, it is also possible for a developer to explicitly store objects into View State and retrieve them again when needed using a key to identify the data being stored (see Example 21.3, "Explicitly storing and retrieving a small piece of data in View State"). You'll need to cast the View State data back to its correct type when you retrieve it.

View State is a good place to persist small bits of data such as entity key information or TimeStamp values, but it is not advisable to use View State to persist entire entity objects, graphs, or even collections of entities.

Example 21.3. Explicitly storing and retrieving a small piece of data in View State

Page.ViewState("myKey")=myCustomer.EntityKey
custkey = CType(ViewState("custKey"), EntityKey)
Page.ViewState["myKey"]=myCustomer.EntityKey;
custkey = (EntityKey)(ViewState["custKey"]);

The EntityDataSource control uses View State behind the scenes to retain entity values; most importantly, the original values of the entity being edited so that when it's time to call SaveChanges, the context has access to Original and Current values and is able to build update commands based on them.

Although the focus of this chapter is on pulling the ObjectContext out of the UI layer, you may be curious about pushing entities into View State. ObjectContext is thankfully not an option, because it is not serializable.

Figure 21.4, "Half of the View State for a page that contains only a single Customer entity" shows the source for a simple page where a single Customer entity is being retrieved and put into View State. The only control on the page is a TextBox to serve as a basis for measuring View State and a button to provide a way to force a postback on the page. The figure shows a screenshot of only half of the source of the page. The other half is filled with the View State as well.

Figure 21.4. Half of the View State for a page that contains only a single Customer entity

Half of the View State for a page that contains only a single Customer entity

When the customer is not being stored in View State, the size of the page is about 1,000 bytes. With the single customer stored into View State the size of the page grows to nearly 11,400 bytes. That is a pretty significant amount of data. And you can see in Table 21.1, "Impact of Customer entities on View State" how it could easily grow even larger. You need to watch out for this with any objects that you persist in View State, such as data sets, not just entities.

Table 21.1. Impact of Customer entities on View State

Customer entities in View State

Size of page (bytes)

0

1,038

1

11,380

20

44,588

50

97,676


This gives you a good idea of the potential impact of storing entities in View State. As the View State increases, the time it takes to deliver the page to the browser also increases. The first entity stored in View State has additional data included in it. After that, the additional Customer entities are only about 1,750 bytes each. You may not want to incur this additional cost in your applications.

Application cache and session state

The most common alternative to retaining objects in memory is to store them in the server's memory using either the application cache or ASP.NET session state.

Application cache is used to retain data that is accessed frequently but does not change frequently. More importantly, application cache is shared across all active sessions of a web application. It does not provide unique storage of memory for each user. Instead every user would be working with the same set of data. You could use application cache for read-only data that you want to share among users and you'll see a pattern for doing this further on in this chapter. However if that data needs to participate in relationships with data that the users are editing, which would require them to be managed by the same ObjectContext, then this wouldn't be feasible. Finally, you probably do not want to consider editing data that is being retained in the application cache unless you have a very specific need and are confident that you will have precise control over the interaction.

With session state, however, ASP.NET preserves a cache of memory on the server for every user currently accessing your website using a class called Session. Not only is session state a great way to retain information in memory, but as a user moves from page to page in your web application, the session state remains available. When the user ends her session with your website, that chunk of memory is disposed. Session state is most commonly accessed through a Page class, but you can also use it from a business layer.

Like View State, session state can only store objects that are serializable, and like View State it can grow out of control if you are not paying attention, but in a way that can be worse than an individual user's View State.

Although View State offloads this information to the browser, session state puts all of the stored information for every user hitting the website on the server. If 10 people are using your website, the server needs to store the session state for those 10 people in memory. If 1,000 people are using your website, imagine how much of the server's memory you might need to store all of their session state information. If you are concerned with scalability, you should use session state wisely.

Note
For websites that scale out dramatically, to the extent that multiple servers are used, session state can become problematic as a user may not hit the same server after a postback. In this case, your options will be to hit the database (using a ClientWins Refresh or requery) or to use one of the other session state solutions available for ASP.NET. The latter is a topic to be researched in the many resources and books that are dedicated to ASP.NET and website performance.

Another important thing to realize regarding session state is that a lot of effort is required for ASP.NET to move objects in and out of Session.

In the long run, session state is a very tempting place to store data, but you should use it for storing only small amounts of information as it comes with a lot of baggage.

ASP.NET provides another option which is a reasonable balance between achieving a near-perfect n-tier architecture and getting the job done without a huge amount of complexity-the ObjectDataSource control.

The ObjectDataSource control was introduced in ASP.NET 2.0. It acts as a bridge between data-bound server controls and your own classes that provide CRUD (Create, Read, Update, and Delete) methods for interacting with your business objects. With an ObjectDataSource control, you can point to a class and hook up its various CRUD methods to the Select, Insert, Update, and Delete events of the ObjectDataSource. The method that you tie to the ObjectDataSource.Select event is expected to return an IList, which can contain one or more objects. ObjectDataSource's Insert, Update, and Delete events are designed to work with a single object, not a collection. Once the ObjectDataSource is wired up to this class, you can use the ObjectDataSource as the DataSource of a server that is data-binding controls, such as GridView or FormView. Data-binding controls can automatically format themselves based on the properties of the object exposed through the ObjectDataSource.

Why ObjectDataSource?

ObjectDataSource relieves the UI developer of myriad complexities when trying to bind objects to web server controls. You'll see that to use ObjectDataSource with entities you'll need to execute a number of tricks; but in the long run, it is still a great deal simpler than trying to orchestrate updatable data binding manually.

Figure 21.5, "ObjectDataSource's CRUD properties pointing to the business layer's CRUD methods" shows how the ObjectDataSource fits into an application where it is used to serve up Contact entities. The class that provides the CRUD methods in this image is named ContactProvider. It will use an ObjectContext to retrieve and save changes to Contact entities.

Figure 21.5. ObjectDataSource's CRUD properties pointing to the business layer's CRUD methods

ObjectDataSource's CRUD properties pointing to the business layer's CRUD methods

ObjectDataSource Enforces Its Rules on Your Objects

This class will be quite different from the class we discussed in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications. ObjectDataSource can interact with only a single type, such as Contact. So although ObjectDataSource simplifies data binding, especially for pages with master detail information, you'll need a separate ObjectDataSource for each type you are working with and a separate set of CRUD methods in your provider class or classes. The Insert, Update, and Delete methods that you bind it to will operate on only a single object, resulting in a lot of calls to the server as well as to the database.

Because of this, when using an ObjectDataSource, you will not be able to take advantage of entity graphs or relationships or ObjectContext's ability to retain change tracking for many entities at once. It's not impossible, but you would have to do so many tweaks that it will eliminate the value of the control.

ObjectDataSource and Its Many Calls to the Database

One of the advantages of the Entity Framework is that you can query for a graph in a single call to the database and update the full graph as well. However, when using ObjectDataSources to populate a web page with related data, you will query for and update those objects separately. This means a lot of hits to the database.

Whether this is a bad thing is relative and depends on your application, your users, your resources, and your needs. The many single calls to the database are part of the paradigm of the stateless web. Although client-side applications are expected to work with data as a group-for example, edit a customer and the customer's orders and then call one update-web users are used to working with records individually. On top of this, the web page is already hitting the server every time the user clicks on an Insert, Update, or Delete button. With the default controls, even a Cancel button causes a call to the server.

How the ObjectDataSource Gets Data into and out of the Data-Binding Controls

Consider a master/detail page using the BreakAway data where you might use a DetailsView to display the parent record (e.g., Customer) and a GridView to display a collection of children (e.g., Reservations).

You would bind the DetailsView to an ObjectDataSource that you have linked to a Customer provider with Get/Update/Insert and Delete methods for Customer entities. ASP.NET automatically wires the DetailsView events to the ObjectDataSource methods. So, when a user updates a record in the DetailView, that control automatically calls the ObjectDataSource's Update method. It then takes the values in the data control and passes them to the Update method in the business layer.

This happens separately for each control. If you have the master object, Customer, displayed in the DetailsView control, and then Customer.Reservations in another control, the Reservations would need its own ObjectDataSource that hooks up to methods that retrieve, insert, update, and delete reservations. You can see this in Figure 21.6, "Each ObjectDataSource on a page working with a different business object".

Figure 21.6. Each ObjectDataSource on a page working with a different business object

Each ObjectDataSource on a page working with a different business object

To plan what object provider classes you will want to design, you should have an idea of what objects the web page will require.

The example web page that you'll create, shown in Figure 21.7, "The Customer data entry form prior to the input of the talented web designer", is for BreakAway customers to update their personal data and view their reservation information. The form will contain three ObjectDataSources. The personal information from the Customer entity is displayed in a DetailsView that is hooked up to one of the ObjectDataSources and can be edited. The Customer's Reservations will be displayed in a ListView control and will be read-only. Addresses will be displayed in a tiled ListView control. The user can add, edit, or delete addresses. The Reservations and Addresses ListViews are hooked up to the other two ObjectDataSource controls.

Because the customers will be able to edit their personal data, this means you'll need to provide the Activity and Destination lists so that they can select their favorites. A separate class will be used to provide these read-only lists.

Figure 21.7. The Customer data entry form prior to the input of the talented web designer

The Customer data entry form prior to the input of the talented web designer

In all, the solution requires four new classes: CustomerProvider, AddressProvider, ReservationProvider, and ListProvider.

First, we'll write these classes, and then you'll get to see how to create and wire up the ObjectDataSource.

Creating the CustomerProvider Class

Example 21.4, "The CustomerProvider class" lays out the class that acts as the Customer provider that will work with an ObjectDataSource.

Note
You may want to incorporate these methods into a class that you can use with a variety of UIs. But keep in mind that because ObjectDataSource forces you to deal with one object at a time, these may not be useful in other applications where you'll want to work with related data graphs in a single ObjectContext.

Because customers are not deleted in the BreakAway business, there is no method for delete. Nor is there an insert method, because this web UI is meant to be used by existing customers. The AddressProvider, further on, has these additional methods.

An important feature of this class is that it implements IDisposable. This is a requirement for it to be used by ObjectDataSource. IDisposable allows a class to be explicitly disposed by classes that instantiate it, rather than waiting around for the garbage collector to see that it's no longer being used.

There are a few notable differences from the class you built for the Windows form in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications. Because the ObjectContext is short-lived, there's no need to worry about how incoming data should be merged because the context will always be brand-new when a query is executed and it won't contain any preexisting entities. Also, to allow the UI to display the names of the navigation properties (e.g., the ActivityName from PrimaryActivity) those navigations are included in the query. Only existing customers will use this website; therefore, there is no need for the method to convert a contact to a customer as you did in the smart client in the previous chapter.

Note
Although the context and the class are short-lived, the web application is not. Precompiling LINQ queries is a very good idea for websites. The query in GetCustomer uses the Where query builder method (you can tell because of the Entity SQL clause). Therefore, an Entity SQL expression will be created and will be reused from the query plan cache. Remember that not every Entity SQL expression falls into the scope of what will benefit from caching, so if you are unsure about what to do, use a LINQ query instead and precompile it.

Notice in Example 21.4, "The CustomerProvider class" that a graph is being retrieved. The method associated with ObjectDataSource's Read can return a graph. The limitation for single entities is for the Insert, Update, and Delete methods. Therefore, you can display the related data that is in the graph, but you only update the primary entity. You will be able to get to Customer.PrimaryActivity.ActivityName and the other reference properties because the full graph will be returned to the web page.

Notice that the class leverages the CommandExecutor class that you built in the previous example.

Note
To benefit, add overloads to the GetList, GetFirstorDefault, and GetReferenceList methods in the CommandExecutor to allow you to pass in a MergeOption. The entities created in the following classes will be immediately detached from the context when the page is disposed. Therefore, there is no point in wasting the extra effort to create ObjectStateEntry objects for the entities. The code listings that follow do not demonstrate this, however the download on the book's website does.

Example 21.4. The CustomerProvider class

Imports BAGA
Imports System.Data.Objects

Public Class CustomerProvider
  Implements IDisposable

  Private _commonContext As BAEntities
  Private _dal As DAL.CommandExecutor

  Public Sub New()
    _commonContext = New BAEntities
    _dal = New DAL.CommandExecutor
  End Sub

  Public Function GetCustomer (ByVal ContactID As Int32) As Customer

  'include Entity References to get ActivityName and DestinationName
    Dim custs = _commonContext.Contacts.OfType(Of Customer) _
                              .Include("PrimaryActivity") _
                              .Include("SecondaryActivity") _
                              .Include("PrimaryDestination")
                              .Include("SecondaryDestination") _
                              .Where("it.Contactid=" & ContactID)
   'return the object, not the query
    Return _dal.ExecuteFirstOrDefault(custs)
  End Function


  Public Sub UpdateCustomer(ByVal Customer As Customer, _
                            ByVal orig_Customer As Customer)
    Try
     'build entitykey from incoming contactID so that you can Attach
      Dim ekey = New EntityKey("BAEntities.Contacts", _
                               "ContactID", orig_Customer.ContactID)
      orig_Customer.EntityKey = ekey

      _commonContext.Attach(orig_Customer)

     'don't let certain props ever get overwritten
     'this code is unnecessary if these properties' setters were marked
     'Internal 
      Customer.AddDate = orig_Customer.AddDate
      Customer.InitialDate = orig_Customer.InitialDate

      _commonContext.ApplyPropertyChanges("Contacts", Customer)

    'ApplyPropertyChanges does not affect navigation properties
    'You need to update manually
      With orig_Customer
        .PrimaryActivityReference.EntityKey = _
          Customer.PrimaryActivityReference.EntityKey
        .SecondaryDestinationReference.EntityKey = _
          Customer.SecondaryDestinationReference.EntityKey
        .PrimaryDestinationReference.EntityKey =  _
          Customer.PrimaryDestinationReference.EntityKey
        .SecondaryDestinationReference.EntityKey = _
          Customer.SecondaryDestinationReference.EntityKey
      End With

      'call the custom SaveAllChanges method
      _commonContext.SaveAllChanges()
    Catch ex As Exception
     'insert proper exception handling here... don't just throw it
      Throw 
    End Try
  End Sub

End Class
using BAGA;
using System.Data.Objects;
namespace BAGA.ObjectProviders
{
  public class CustomerProvider : IDisposable
  {
    private BAEntities _commonContext;
    private DAL.CommandExecutor _dal;
    public CustomerProvider()
    {
        _commonContext = new BAEntities();
        _dal = new DAL.CommandExecutor();
    }

    public Customer GetCustomerwithRelatedData(Int32 ContactID)
    {
      var custs = _commonContext.Contacts.OfType<Customer>()
                               .Include("PrimaryActivity")
                               .Include("SecondaryActivity")
                               .Include("PrimaryDestination")
                               .Include("SecondaryDestination")
                               .Where("it.Contactid=" + ContactID);
      return _dal.ExecuteFirstorDefault(custs);
      }
    public void UpdateCustomer(Customer Customer, Customer orig_Customer)
    {
      try
      {
        //build entitykey from incoming contactID so that you can Attach
        var ekey = new EntityKey("BAEntities.Contacts", 
                                 "ContactID", orig_Customer.ContactID);
        orig_Customer.EntityKey = ekey;
        _commonContext.Attach(orig_Customer);

        //don't let certain props ever get overwritten 
        Customer.AddDate = orig_Customer.AddDate;
        _commonContext.ApplyPropertyChanges("Contacts", Customer);

        //applypropchanges does not affect navigation properties
        //critical to have original reference id
        //SaveChanges uses that when building the update cmd
        orig_Customer.PrimaryActivityReference.EntityKey = 
          Customer.PrimaryActivityReference.EntityKey;
        orig_Customer.SecondaryActivityReference.EntityKey = 
          Customer.SecondaryActivityReference.EntityKey;
        orig_Customer.PrimaryDestinationReference.EntityKey = 
          Customer.PrimaryDestinationReference.EntityKey;
        orig_Customer.SecondaryDestinationReference.EntityKey = 
         Customer.SecondaryDestinationReference.EntityKey;
        //SavingChanges event takes care of Modified Date
        _commonContext.SaveAllChanges();
      }
      catch (Exception ex)
      {
        //insert proper exception handling here...
        throw ex;
      }
    }
  }
} //end of root namespace

In the GetCustomer method, pay special attention to the fact that the method returns an object, not the query. If you returned the query, it will be executed when the page begins to render the control to which the query is bound. This will fail because the ObjectContext instantiated in the CustomerProvider object will be out of scope.

The UpdateCustomer method does a number of things that are specific to how the Entity Framework works.

First, it takes an original version of the object and the current version. The ObjectDataSource has a means to provide both of these values. This Customer exists in the database, so we need to Attach to the context, not Add. However, Attach requires an EntityKey, so you need to create that using the ContactID. This is because the Customer will be created from scratch using the values in the data-binding control on the UI side. You'll see the UI code-behind after of the class provider code listings.

Note
Remember that in the BreakAway model, Customer inherits from Contact and therefore has the key property, ContactID, not CustomerID.

The next notable code is an update of the AddDate and InitialDate properties. In case those are used in the UI, you want to make sure that the user or UI developer doesn't attempt to update those values. The simplest thing is to just be sure the original and new values are equal and SaveChanges will ignore them.

Controlling Property Scope with Getter and Setter

You can control access to individual entity properties by modifying the Getter and Setter properties of an Entity property in the Entity Data Model Designer. By setting the Setter property of the Customer.InitialDate and Contact.AddDate properties to Internal, when the Customer entity class is generated the SET for the class property will be marked Internal. Only code in the same assembly can modify the property.

But how will the values ever be set on new Customers and Contacts from external assemblies such as this one? In Chapter 9, Working with Object Services, you added code to the SavingChanges call that ensured that new Contacts had their AddDate populated and new Customers had their CustomerTypeReference populated with a default value. You can add logic to also ensure that Customer.InitialDate is set.

These modifications are performed in the same assembly where the Customer class and the Contact class live, and would therefore be allowed by the Internal access of the fields. Then you won't have to worry about code in external assemblies modifying those fields inadvertently.

Pay attention to the reminder that ApplyPropertyChanges affects only scalar values, which is why the method updates the EntityReference values manually.

The call to the SaveAllChanges custom property that you wrote in Chapter 18, Handling Entity Framework Exceptions will handle concurrency exceptions. If SaveAllChanges has anything to report back to this class, you can catch it in the exception handler.

Creating the AddressProvider Class

The AddressProvider class (see Example 21.5, "The AddressProvider class") contains all four CRUD operations: GetAddresses, UpdateAddress, InsertAddress, and DeleteAddress. The Get and Update methods are similar to those in the CustomerProvider.

This class also contains a few notable bits of code that are discussed after the code listing.

Example 21.5. The AddressProvider class

Imports BAGA
Imports System.Data.Objects

Public Class AddressProvider
  Implements IDisposable
  Private _commonContext As BAEntities
  Private _dal As DAL.CommandExecutor

  Public Sub New()
    _commonContext = New BAEntities
    _dal = New DAL.CommandExecutor
  End Sub

  Public Function GetAddresses(ByVal contactID As Int32) _
   As List(Of Address)
    Dim addresses = _commonContext.Addresses _
                    .Where("it.Contact.ContactID=" & contactid)
    _dal.ExecuteList(addresses)
  End Function

  Public Sub UpdateAddress _
   (ByVal Address As Address, ByVal orig_Address As Address)
    Try
     'build entitykey
      Dim ekey = New EntityKey("BAEntities.Addresses", _
                               "addressID", orig_Address.addressID)
      orig_Address.EntityKey = ekey
      _commonContext.Attach(orig_Address)

     'no navigation properties or other fields to worry about
     'SavingChanges custom event takes care of ModifiedDate
      _commonContext.ApplyPropertyChanges("Addresses", Address)
      _commonContext.SaveAllChanges()
    Catch ex As Exception
      'add proper exception handling
      Throw
    End Try
  End Sub

  ''' <summary>
  ''' Inserts new Address. ContactID property must be set
  ''' </summary>
  ''' <param name="Address"></param>
  Public Sub InsertAddress(ByVal Address As Address)
    Try
      _commonContext.AddToAddresses(Address)
      _commonContext.SaveAllChanges()
    Catch ex As Exception
      'add proper exception handling
      Throw
    End Try
  End Sub

  Public Sub DeleteAddress(ByVal Address As Address)
    Try
      Address.EntityKey = New EntityKey("BAEntities.Addresses", _
                                        "addressID", Address.addressID)
      _commonContext.Attach(Address)
      _commonContext.DeleteObject(Address)
      _commonContext.SaveAllChanges()
    Catch ex As Exception
     'add proper exception handling
      Throw ex
    End Try
  End Sub
End Class
public class AddressProvider : IDisposable
{
  private BAEntities _commonContext;
  private DAL.CommandExecutor _dal;
  public AddressProvider()
  {
    _commonContext = new BAEntities(); 
    _dal = new DAL.CommandExecutor();
  }

  public List<Address> GetAddresses(Int32 contactid)
  {
    var addresses = _commonContext.Addresses
                    .Where("it.Contact.ContactID=" + contactid);
    return _dal.ExecuteList(addresses);
  }

  public void UpdateAddress(Address Address, Address orig_Address)
  {
    try
    {
      //build entitykey
      var ekey = new EntityKey("BAEntities.Addresses",
                               "addressID", orig_Address.addressID);
      orig_Address.EntityKey = ekey;

      _commonContext.Attach(orig_Address);
      //no navigation properties or other fields to worry about
      //SavingChanges event takes care of Modified Date
      _commonContext.ApplyPropertyChanges("Addresses", Address);
      _commonContext.SaveAllChanges();
    }
    catch (Exception ex)
    {
      //add proper exception handling
      throw;
    }
  }

  public void InsertAddress(Address Address)
  {
    Debug.Assert(Address.ContactID != null,
                "Address.ContactID must be set");
    try
    {
      _commonContext.AddToAddresses(Address);
      _commonContext.SaveAllChanges();
    }
    catch (Exception ex)
    {
      //add proper exception handling
      throw;
    }

  }
  public void DeleteAddress(Address Address)
  {
    try
    {
      Address.EntityKey = new EntityKey(
       "BAEntities.Addresses", "addressID", Address.addressID);
      _commonContext.Attach(Address);
      _commonContext.DeleteObject(Address);
      _commonContext.SaveAllChanges();
    }
    catch (Exception ex)
    {
      //add proper exception handling
      throw;
    }
  }
}

The Insert method has a Debug.Assert in it. An address requires a ContactReference. If it's not included, an exception will be thrown during the SaveChanges call as the Entity Framework checks referential constraints. Debug.Assert will test this during debug, and if the assertion fails, a message will be output to the debug window.

When the UI instantiates the AddressProvider to request an update, the AddressProvider has no idea which Customer is being edited on the UI side. In the UI, the Customer object is not available when the Address is being inserted by its ObjectDataSource. It puts the onus on the UI developer to provide the Customer's EntityKey in the Address.CustomerReference before passing the Address back to the AddressProvider.

However, it seems unfair to force the UI developer to know this type of detail about entities. What's a reference? What's an EntityKey? It would be convenient if the more familiar foreign key, ContactID, was available directly in the address. You can solve this by creating a property in the partial class for Address in the model that does this work on the fly. All it requires is that the UI developer passes in the ContactID, which he will have access to. This property will take the ContactID and create the ContactReference.EntityKey. You'll find this to be a convenient addition to the Address entity and you may want to implement the concept elsewhere in your entities. In fact, you'll be leveraging this in the Customer class to set and expose the ID for PrimaryActivity and the other customer preferences so that the UI developer has easy access to those as well. For the DebugAssert to check this property, it also needs a getter. Example 21.6, "A ContactID property added into the Address entity's partial class" shows the new ContactID property that you can add to the partial class for the Address entity in the BreakAwayModel project.

Example 21.6. A ContactID property added into the Address entity's partial class

Public Property ContactID() As Integer
  Get
    If ContactReference.EntityKey.EntityKeyValues.Count > 0 Then
      Return CType( _
       ContactReference.EntityKey.EntityKeyValues(0).Value, Integer)
    Else
      Return Nothing
    End If
  End Get

  Set(ByVal value As Integer)
    If value > 0 Then
      ContactReference.EntityKey = _
       New EntityKey("BAEntities.Contacts", "ContactID", value)
    End If
  End Set
End Property
public int ContactID
{
  get 
  {
    if (ContactReference.EntityKey.EntityKeyValues.Count() > 0)
      return (int)ContactReference.EntityKey.EntityKeyValues[0].Value;
    else
      return null;
  }
  set
  {
    if (value > 0)
       ContactReference.EntityKey = 
        new EntityKey("BAEntities.Contacts", "ContactID", value);
  }
}

Now the assert will be able to use this, and when we get to the UI, you'll see it in action there as well.

In the Delete method, remember that DeleteObject won't work unless the object is already in the context, so you need to query for it using the AddressID, delete it, and then finally call SaveAllChanges.

The class is pretty straightforward otherwise.

Creating the ReservationsProvider Class

In the target website, the reservations will be read-only. Therefore, the ReservationsProvider class has only one method, GetReservations, which returns a list of reservations for the given customer (see Example 21.7, "The GetReservations method").

Example 21.7. The GetReservations method

Public Function GetReservations(ByVal contactID As Integer) _
 As List(Of Reservation)
   Dim res = _commonContext.Reservations _
             .Include("Trip.Destination") _
             .Include("UnpaidReservation") _
             .Where("it.Customer.ContactID=" & contactid)
   Return _dal.ExecuteList(res)
End Function
public List<Reservation> GetReservations(int contactid)
{
 var res = _commonContext.Reservations
                         .Include("Trip.Destination")
                          .Include("UnpaidReservation")
                          .Where("it.Customer.ContactID=" + contactid);
  return _dal.ExecuteList(res);
}

You can always add in the other CRUD methods if you plan to reuse the class elsewhere.

Providing Reusable, Cached Reference Lists

This web application needs the same drop-down lists as the Windows Forms application in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications. We'll take the GetReferenceList method that you built in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications and enhance it so that it is able to cache the lists. That way, the database call required for these lists won't be repeated for each user of the application.

Note
The new pattern is similar to one you might use for any caching strategy and not necessarily specific to Entity Framework. As with other ASP.NET caching strategies, you will likely want to enable periodic refreshing of these lists. Note that unfortunately, the SQL Query Notification feature that was added to ASP.NET 2.0 and ADO.NET 2.0 does not work with Entity Framework. This is driven by the fact that the necessary mechanism required of the database does not exist in all databases and therefore Entity Framework cannot leverage the feature.

First you should take advantage of the web application's application cache to store the lists. Why run the same queries for the same data over and over again and keep hitting the database? Instead, once the lists have been retrieved from the database, they are stored in the application class using a static (Shared in VB) variable. Even as the ListProvider is disposed and reinstantiated, that data remains in the application cache.

The only publicly exposed member of the ListProviders is the RefLists property, which stores the individual lists as they are created and returns them to the caller when requested. This is the property that is exposed as a static variable.

The static variable, RefLists, is a List of reference lists. The ListProvider.GetReferenceList checks to see if the requested list, that is a list with the requested entity type, exists in the RefLists instance. If it does, then GetReferenceList returns the desired list. If not, the helper method, AddNewRefList is called. The helper method creates and executes the necessary queries by calling the CommandExecutor's GetReferenceList method that you wrote in Chapter 20, Using the Entity Framework in n-Tier Client-Side Applications, and then adds the resulting List to RefLists. After this, GetReferenceList is able to return the new list to the calling code.

Because RefLists is static/Shared, it remains in the application memory, indifferent to the ListProvider class being instantiated and disposed. When a new instance of ListProvider is created, the preexisting RefLists is accessed from the application memory. Example 21.8, "The ListProvider class" shows the complete listing for the ListProvider class which serves up reference lists for the user interface.

Example 21.8. The ListProvider class

Public Class ListProvider
  Implements IDisposable
  Private _commonContext As BAEntities
  Private _dal As BAGA.DAL.CommandExecutor
  Private Shared RefLists As List(Of IList)
  Public Sub New()
    _commonContext = New BAEntities()
    _dal = New BAGA.DAL.CommandExecutor()
    RefLists = New List(Of IList)()
  End Sub
  Public Function GetReferenceList(Of TEntity)(ByVal orderBy As String) _
   As IList
    'check to see if the list exists already
    For Each enumList As IList In RefLists
      If TypeOf enumList(0) Is TEntity Then
        Return enumList
      End If
    Next enumList
    'if we are here, then the list was not found
    Dim newlist = AddNewRefList(Of TEntity)(orderBy)
    Return newlist
  End Function
  Private Function AddNewRefList(Of TEntity)(ByVal orderBy As String) As IList
    If GetType(TEntity) Is GetType(Trip) Then
      Dim tripQuery = _
       From t In _commonContext.Trips.Include("Destination") _
       Order By t.Destination.DestinationName, t.StartDate _
       Select t
      Dim newlist As List(Of TEntity) = _
       _dal.ExecuteList(Of Trip)(tripQuery).Cast(Of TEntity).ToList
      RefLists.Add(newlist)
      Return newlist
    Else
      Dim newlist = _dal.GetReferenceList(Of TEntity)(_commonContext, orderBy)
      RefLists.Add(newlist)
      Return newlist
    End If
  End Function
End Class
public class ListProvider : IDisposable
{
  private BAEntities _commonContext;
  private BAGA.DAL.CommandExecutor _dal;
  static List<IList> RefLists;

  public ListProvider()
  {
    _commonContext = new BAEntities();
    _dal = new BAGA.DAL.CommandExecutor();
    RefLists = new List<IList>();
  }

  public IList GetReferenceList<TEntity>(string orderBy)
  {
     //check to see if the list exists already
    foreach (IList enumList in RefLists)
    {
      if (enumList[0] is TEntity)
        return enumList;
    }
    //if we are here, then the list was not found
    var newlist= AddNewRefList<TEntity>(orderBy);
    return newlist;
  }
        
  private IList AddNewRefList<TEntity>(string orderBy)
  {
    if (typeof(TEntity) == typeof(Trip))
    {
      var tripQuery =
          from t in _commonContext.Trips.Include("Destination")
          orderby t.Destination.DestinationName, t.StartDate
          select t;
      List<TEntity> newlist= _dal.ExecuteList<Trip>.Cast<TEntity>().ToList();
      RefLists.Add(newlist);
      return newlist;
    }
    else
    {
      var newlist = _dal.GetReferenceList<TEntity>(_commonContext, orderBy);
      RefLists.Add(newlist);
      return newlist;
    }
  }
}

Although wiring up the classes to the ObjectDataSource controls' properties is no different from wiring up any other business classes you may use with ObjectDataSource, you will need to take some action in some of the ObjectDataSource controls' events specifically because you are working with entities. Additionally, the data-bound controls will need some special tweaks as well.

Using the ObjectDataSource Wizard to Perform the Initial Wiring to the Provider Classes

If you are unfamiliar with using an ObjectDataSource control, it has a wizard-the ObjectDataSource Wizard-which helps with the basics of wiring up the class and the methods. When you create a new ObjectDataSource, a drop-down list shows all of the classes that are available. If your provider classes are in a different assembly, you'll need a reference to that assembly. Once you have selected the provider class, the wizard walks you through selecting which methods in that class to associate with the different actions-Select, Update, Insert, and Delete. You need to wire up only the actions you want to use. So, in the CustomerDataSource, you only need to identify the methods for Select and Update.

Note
ObjectDataSource newbies can find lots of resources and walkthroughs in the MSDN docs and elsewhere.

Tweaking ObjectDataSource Properties to Work with Entities

Once you have walked through the wizard, you need to take some important steps to modify the properties of the new ObjectDataSource controls so that they can work with the BreakAway entities.

We'll use the CustomerDataSource as an example.

Getting the object to the entity provider

The ObjectDataSource needs to be able to automatically construct the proper objects using the values in the binding controls. Specify the class using its fully qualified name, in the DataObjectTypeName property BAGA.Customer.

Note
If you don't specify the DataObjectTypeName property and the ObjectDataSource needs it to construct a desired class, you will get an exception. If your class methods require special parameters, you'll need to leave the property empty and build those parameters in the appropriate event. This example does not use parameters other than the classes.

Providing original and current values for updates

Because the Entity Framework requires original values in order to properly execute SaveChanges, you'll need to set the ObjectDataSource.ConflictDetection property to CompareAllValues. This will tell the ObjectDataSource to send both the current version of the object and a version containing the original values.

Use the ObjectDataSource.OldValuesParameterFormatString property to designate a string to append to the object name when constructing the original version of the object. A typical setting for this is orig_{0}, which will name the object containing original values to orig_Customer when the object itself is Customer.

These are the only additional settings you'll need to make to the ObjectDataSource.

Using ObjectDataSource Events to Solve Problems That Are Particular to Entities

Although a good portion of the work that is necessary to get values from the binding control to the entity providers is automated, you need to do some special things because of the way in which entities function.

Inserting and updating entities that contain EntityReferences

Recall that EntityDataSource is able to flatten navigation properties as they get to a databinding Control. The ObjectDataSource is not able to do that. This means you'll have to explicitly do some additional binding in the markup, and it also means those properties won't automatically make the return trip as they did with the EntityDataSource, so you'll need to do some manual labor in the code-behind to get around this.

Customer has five properties that are EntityReferences: CustomerType, PrimaryActivity, SecondaryActivity, PrimaryDestination, and SecondaryDestination. We'll focus on the last four.

The query in the CustomerProvider class included the navigations for the preference properties.

By default, when you data-bind the control to the ObjectDataSource, BoundField controls will be created for each field in the object. Example 21.9, "BoundField controls for a scalar property" shows the control that displays FirstName.

Example 21.9. BoundField controls for a scalar property

<asp:BoundField DataField="FirstName" HeaderText="FirstName" 
                SortExpression="FirstName" />

But BoundFields do not support navigation paths. Instead, you'll need to use a TemplateField that uses DataBinder.Eval for the binding. Example 21.10, "A TemplateField, which can be used for navigation properties" shows how ActivityName from the PrimaryActivity navigation property is displayed with an ItemTemplate.

Example 21.10. A TemplateField, which can be used for navigation properties

<asp:TemplateField HeaderText="Favorite Activity">
  <ItemTemplate>
   <asp:Label ID="Label1" runat="server"
               Text='<%# Databinder.Eval(Container.DataItem,
                         "PrimaryActivity.ActivityName") %>'>
    </asp:Label>
  </ItemTemplate>
</asp:TemplateField>

The ItemTemplate is used for the read-only view of the control. Additionally, an EditItemTemplate is required if you plan for users to edit the value. You can use a drop-down list in an EditItemTemplate so that users will be able to select from a list of activities.

Example 21.11, "A TemplateField with both ItemTemplates to display and edit" shows the complete TemplateField, including both the ItemTemplate and the EditItemTemplate.

Example 21.11. A TemplateField with both ItemTemplates to display and edit

<asp:TemplateField HeaderText="Favorite Activity">
  <EditItemTemplate>
    <asp:DropDownList runat="server" id="act1DDL">
    </asp:DropDownList>
  </EditItemTemplate>
  <ItemTemplate>
    <asp:Label ID="Label1" runat="server"
               Text='<%# DataBinder.Eval(Container.DataItem,
                         "PrimaryActivity.ActivityName") %>'>
    </asp:Label>
  </ItemTemplate>
</asp:TemplateField>

Notice that the EditItemTemplate has no binding. All of this needs to be done in the code-behind of the page. Let's look at how that works.

Editing EntityReference navigation properties

The only time the lists are needed is when the user is about to edit a customer record. In the example, a DetailsView control is used to display the customer. Therefore, the time to retrieve and bind the lists is when DetailsView is being rebuilt for the edit view. This translates to: after the user has clicked the Edit button and the page is in the process of posting back with the intention of rendering the DetailsView in Edit mode.

The point in the page events where you can insert your logic to bind the lists from the ListProvider to the DropDownLists in the UI is in the DetailsView.PreRender event.

In this event, you'll first need to check to see whether the DetailsViewMode is Edit. If it is, a new ListProvider object is instantiated to get the lists of destinations and activities. To bind to the drop-downs, you need to extract each drop-down list control from within the DetailsView using the FindControl method. It's tricky work, but the code listing in Example 21.12, "Binding the drop-downs at runtime" will do the job for you.

Note
This method uses EntityReference values which have been stored into ViewState in another method. You'll see the code that does this further on.

Example 21.12. Binding the drop-downs at runtime

Protected Sub custDV_PreRender(ByVal sender As Object, ByVal e As EventArgs)
  'only get lists when in Edit mode
  If custDV.CurrentMode = DetailsViewMode.Edit Then
	'get lists
	Dim dal As New DAL.ListProvider()
	Dim actList = dal.GetReferenceList(Of Activity)("ActivityName")
	Dim destList = dal.GetReferenceList(Of Destination)("DestinationName")

	'find drop downs in markup
	Dim ddlA1 = CType(custDV.FindControl("act1DDL"), DropDownList)
	Dim ddlA2 = CType(custDV.FindControl("act2DDL"), DropDownList)
	Dim ddlL1 = CType(custDV.FindControl("loc1DDL"), DropDownList)
	Dim ddlL2 = CType(custDV.FindControl("loc2DDL"), DropDownList)

	'call custom method to bind the lists to the dropdowns 
	BindPickList(ddlA1, actList, CInt(ViewState("origAct1ID")), _
                 "ActivityID","ActivityName")
	BindPickList(ddlA2, actList, CInt(ViewState("origAct2ID")), _
                "ActivityID","ActivityName")
	BindPickList(ddlL1, destList, CInt(ViewState("origDest1ID")), _
                "DestinationID","DestinationName")
	BindPickList(ddlL2, destList, CInt(ViewState("origDest2ID")), _
                "DestinationID", "DestinationName")
  End If
End Sub

Private Sub BindPickList(ByVal ddl As DropDownList, ByVal list As IList, _
                        ByVal ddlValue As Integer, ByVal IDProp As String, _
                        ByVal DisplayProp As String)
  ddl.DataSource = list
  ddl.DataTextField = DisplayProp
  ddl.DataValueField = IDProp
  ddl.DataBind()
  ddl.SelectedValue = ddlValue.ToString()
End Sub
protected void custDV_PreRender(object sender, EventArgs e)
{
  //only get lists when in Edit mode
  if (custDV.CurrentMode == DetailsViewMode.Edit)
  {
    //get lists
    DAL.ListProvider dal = new DAL.ListProvider();
    var actList = dal.GetReferenceList<Activity>("ActivityName");
    var destList = dal.GetReferenceList<Destination>("DestinationName");

    //find drop downs in markup
    var ddlA1 = (DropDownList)(custDV.FindControl("act1DDL"));
    var ddlA2 = (DropDownList)(custDV.FindControl("act2DDL"));
    var ddlL1 = (DropDownList)(custDV.FindControl("loc1DDL"));
    var ddlL2 = (DropDownList)(custDV.FindControl("loc2DDL"));

    //call custom method to bind the lists to the dropdowns 
    BindPickList(ddlA1, actList, (int)ViewState["origAct1ID"],
                "ActivityID","ActivityName");
    BindPickList(ddlA2, actList, (int)ViewState["origAct2ID"],
                "ActivityID","ActivityName");
    BindPickList(ddlL1, destList, (int)ViewState["origDest1ID"],
                "DestinationID","DestinationName");
    BindPickList(ddlL2, destList, (int)ViewState["origDest2ID"],
                "DestinationID", "DestinationName");
  }
}

private void BindPickList(DropDownList ddl, IList list, int ddlValue,
                          string IDProp,string DisplayProp)
{
  ddl.DataSource = list;
  ddl.DataTextField = DisplayProp;
  ddl.DataValueField = IDProp;
  ddl.DataBind();
  ddl.SelectedValue = ddlValue.ToString();
}

BindPickList wraps the redundant data-binding code for each drop-down into a single method. After the list is bound, it also sets the Selected value of the drop-down to the current value of the navigation property.

Figure 21.8, "Editing navigation properties with DropDownLists" displays the result of the binding and shows a section of the form when the DetailsView is in Edit mode.

Figure 21.8. Editing navigation properties with DropDownLists

Editing navigation properties with DropDownLists

Updating EntityReference navigation properties

The ObjectDataSource will not know how to populate the navigation properties when it builds the Customer object for Updates. This problem would exist for Insert and Delete as well if they were being used for Customer objects. Therefore, you will have to lend a helping hand to the Update method of the ObjectDataSource control.

The ObjectDataSourceStatusEventArgs of this method contains an InputParameters property. This property provides references to both the current and original objects. In the Updating method, you can populate the values of the four preference properties before the objects are passed on to the provider class. The current values can be found in the drop down lists while the original values have been stored in ViewState. You accessed these already in the PreRender event. Let's first take a look at the ObjectDataSource's Selected event where the reference values were stored in ViewState in Example 21.13, "Storing the original EntityReference values in View State for later use".

Example 21.13. Storing the original EntityReference values in View State for later use

Private Sub CustomerDataSource_Selected(ByVal sender As Object, _
 ByVal e As System.Web.UI.WebControls.ObjectDataSourceStatusEventArgs) _
 Handles CustomerDataSource.Selected

  Dim cust = CType(e.ReturnValue, BAGA.Customer)
  'dropdown list can't take a null value, so use 0 as a default
  If cust.PrimaryActivity Is Nothing Then
	ViewState("origAct1ID") = 0
  Else
	ViewState("origActID") = cust.PrimaryActivity.ActivityID
  End If

  If cust.SecondaryActivity Is Nothing Then
	ViewState("origAct2ID") = 0
  Else
	ViewState("origActID") = cust.SecondaryActivity.ActivityID
  End If

  If cust.PrimaryDestination Is Nothing Then
	ViewState("origDest1ID") = 0
  Else
	ViewState("origDest1ID") = cust.PrimaryDestination.DestinationID
  End If

  If cust.SecondaryDestination Is Nothing Then
	ViewState("origDest2ID") = 0
  Else
	ViewState("origDest2ID") = cust.SecondaryDestination.DestinationID
  End If
End Sub

private void CustomerDataSource_Selected(object sender,
 System.Web.UI.WebControls.ObjectDataSourceStatusEventArgs e)
{
  var cust = (BAGA.Customer)e.ReturnValue;
  //dropdown list can't take a null value, so use 0 as a default
  if (cust.PrimaryActivity == null)
    ViewState["origAct1ID"] = 0;
  else
    ViewState["origActID"] = cust.PrimaryActivity.ActivityID;
  
  if (cust.SecondaryActivity == null)
    ViewState["origAct2ID"] = 0;
  else
    ViewState["origActID"] = cust.SecondaryActivity.ActivityID;

  if (cust.PrimaryDestination == null)
    ViewState["origDest1ID"] = 0;
  else
    ViewState["origDest1ID"] = cust.PrimaryDestination.DestinationID;

  if (cust.SecondaryDestination == null)
    ViewState["origDest2ID"] = 0;
  else
    ViewState["origDest2ID"] = cust.SecondaryDestination.DestinationID;
}

Now, the values can be retrieved in the CustomerDataSource.Updating event. First you will need to grab the references to the original and current customer, then update the reference properties with the values from ViewState. This is demonstrated in Example 21.14, "Adding reference values to the customer objects in ObjectDataSource.Updating".

Example 21.14. Adding reference values to the customer objects in ObjectDataSource.Updating

Protected Sub CustomerDataSource_Updating(ByVal sender As Object, _
 ByVal e As System.Web.UI.WebControls.ObjectDataSourceMethodEventArgs)

  'cast objects from event args to Customer types
  Dim currCust As var=CType(e.InputParameters(0), Customer)
  Dim origCust As var=CType(e.InputParameters(1), Customer)

  'set ref values on current customer
  currCust.PrimaryActivityID =  _
   CInt((CType(custDV.FindControl("act1DDL"), DropDownList)).SelectedValue)
  currCust.SecondaryActivityID = _
   CInt((CType(custDV.FindControl("act2DDL"), DropDownList)).SelectedValue)
  currCust.PrimaryDestinationID =  _
    CInt ((CType(custDV.FindControl("loc1DDL"), DropDownList)).SelectedValue)
  currCust.SecondaryDestinationID =  _
   CInt((CType(custDV.FindControl("loc2DDL"), DropDownList)).SelectedValue)

  'set ref values on original customer  
  origCust.PrimaryActivityID = CInt(ViewState("origAct1ID"))
  origCust.SecondaryActivityID = CInt(ViewState("origAct2ID"))
  origCust.PrimaryDestinationID = CInt(ViewState("origDest1ID"))
  origCust.SecondaryDestinationID = CInt(ViewState("origDest2ID"))

End Sub
protected void CustomerDataSource_Updating(object sender,
 System.Web.UI.WebControls.ObjectDataSourceMethodEventArgs e)
{
  //cast objects from event args to Customer types
  var currCust=(Customer)e.InputParameters[0];
  var origCust=(Customer)e.InputParameters[1];

  //set ref values on current customer
  currCust.PrimaryActivityID =
   Convert.ToInt32(((DropDownList)(custDV.FindControl("act1DDL"))).SelectedValue);
  currCust.SecondaryActivityID = 
   Convert.ToInt32(((DropDownList)(custDV.FindControl("act2DDL"))).SelectedValue);
  currCust.PrimaryDestinationID = 
   Convert.ToInt32(((DropDownList)(custDV.FindControl("loc1DDL"))).SelectedValue);
  currCust.SecondaryDestinationID =
   Convert.ToInt32(((DropDownList)(custDV.FindControl("loc2DDL"))).SelectedValue);

  //set ref values on original customer  
  origCust.PrimaryActivityID = (int)ViewState["origAct1ID"];
  origCust.SecondaryActivityID = (int)ViewState["origAct2ID"];
  origCust.PrimaryDestinationID = (int)ViewState["origDest1ID"];
  origCust.SecondaryDestinationID = (int)ViewState["origDest2ID"];
}

With this code in place, edits to the reference navigation properties will be included in the update.

Inserting records with EntityReference properties

One last ObjectDataSource event tweak is required to accommodate working with entities. The Update method for Address will be handled completely by the behind-the-scenes work that the ObjectDataSource and data-binding control perform. The Insert and Delete need some help, though. Address has a referential constraint that requires that the ContactReference exists. Although the AddressDataSource is bound to the SelectedValue of the Customer's DetailsView, it won't know to use that value for new addresses. And because of the way deletes work when EntityRefs are involved, the Delete operation will also need the value.

Thanks to the new Address.ContactID property you added to the Address partial class earlier in this chapter, you can set the ContactID of the Address in a single method that covers both the Inserting and Deleting events (see Example 21.15, "Forcing the ContactID value into the Address for inserts and deletes").

Example 21.15. Forcing the ContactID value into the Address for inserts and deletes

Private Sub AddressDataSource_InsertingorDeleting _
 (ByVal sender As Object, ByVal e As ObjectDataSourceMethodEventArgs) _
 Handles AddressDataSource.Inserting, AddressDataSource.Deleting

  Dim add = CType(e.InputParameters("Address"), BAGA.Address)
 'tie address to current customer using custom property
  add.ContactID = custDV.SelectedValue
End Sub
protected void AddressDataSource_InsertingorDeleting(object sender, 
 System.Web.UI.WebControls.ObjectDataSourceMethodEventArgs e)
 {
  var add = (BAGA.Address)(e.InputParameters["Address"]);
  //Insert and Delete both need the ContactReference. Setting 
  //this property will cause the ContactReference.EntityKey to be created
  add.ContactID = (int)custDV.SelectedValue;
}

That's it. Don't forget to wire up this new event to both the Inserting and Deleting events of the AddressDataSource in the C# Windows form.

Note
Because of the large amount of code used in this example, the full code listings in both VB and C# are available on the book's website.

ObjectDataSources simplify data binding when you want to use your own business classes. However, some of the features of the Entity Framework require that you do a little extra work on both ends to make sure everything flows properly. But the simplicity comes with a cost, which is that you don't get to take advantage of many of the Entity Framework's benefits.

What about doing something so that you can work with entity graphs?

With the Entity Framework, a single query can provide the entire graph of the Customer with Addresses and Reservations. A single SaveChanges call can update all of the entities at once.

ObjectDataSource, on the other hand, performs many queries and updates because of the separation of the objects, and so that it can work well with the short page life cycle. Each insert, update, and delete will be followed by a new select.

Database Hits Versus Very Busy Memory

The alternative is to have less database activity and take advantage of View State and session state. The benefits of View State and session state are combined with a lot of drawbacks. The impact of storing the extra data, as explained in the first section of the chapter, isn't the only drawback; rather, with session state you also have to worry about things such as session timeouts and distributed web servers. Additionally, when you store objects into session state, the session state manager must serialize and deserialize each object every time a class that uses session state is instantiated.

Note
For a great look at how session state works and how to get better performance when using it, read the September 2005 MSDN Magazine article "Fast, Scalable, and Secure Session State Management for Your Web Applications" by Mike Volodarsky, a web server guru on the ASP.NET team. You can find it online at http://msdn.microsoft.com/magazine/.

Most guidance that you will find recommends using View State and session state to persist small bits of data across postbacks that are not available elsewhere.

Keep in mind that less database activity doesn't necessarily mean fewer trips to the server. With each retrieval, update, insert, and delete the user requests, the server-side code must still get hit. So, the only thing you are reducing is the number of additional hits from the web server to the database. But you are replacing that with heavy activity in the server's memory, which could be worse.

There have been many impassioned debates about View State versus session state versus no state at all since ASP.NET was released, and the debates continue.

Due to all of the potential issues with attempting to store the objects in memory in a web application, going this route will be a choice for developers who have thoroughly considered the pros and cons of doing so and determined that in their particular scenario, storing the objects in memory and doing the additional work of managing the entities is still preferable. However, this will be a less common solution, whereas the ObjectDataSource should satisfy a majority of your needs.

In this chapter, you learned about the issues you will encounter when using entities in an ASP.NET application where the data and business layers are separated from the UI. Although ASP.NET provides a number of options for working with entities, the prudent choice for many developers is to build a separate class that interacts with the Entity Data Model for each CRUD action the user requests. When you structure the class properly, you can use it easily with an ObjectDataSource on the web page, which simplifies data-binding tasks. However, because the class is specialized for ObjectDataSource, it might not be appropriate to reuse it in other applications.

Entities introduce some new challenges for developers who have already been working with ObjectDataSources-most notably, working with EntityReference navigation properties. The example laid out in this chapter points out where these problems arise and how to solve them.

The alternative to this solution is to pull a graph into memory, work against that, and then call SaveChanges when the user is finished editing the entire graph. This option introduces a lot of complexities that may be acceptable for developers who are building complex websites and need to finely balance every bit of memory access and network activity. But it will take some advanced understanding of ASP.NET state management to achieve that balance.

Now that you have written layers for client applications and web applications, the last scenario is to write a more advanced business layer for using entity graphs in WCF services. This will be the job of the next chapter.

Show:
© 2014 Microsoft. All rights reserved.