5

Restated my Question, old Text Below

As I am still hoping for an answer I would like to restate my question. Image I have a GUI with two lists, one that shows a List of all entries to a database tblOrders and another one that shows the items in each order.

I can use Linq2sql or EF to get all orders from the database, like so:

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

I can display these order in a list or datagridview. Then on a selection of one of the jobs I want to access the collection of entities from the table tblItems. I can do this like so:

ListOfOrders.First().tblItems.toList();

Except I can not, because I need the DataContext which has been disposed. This makes sense in a way, since I can not guarantee that there is not a new items that has been added to that order since I retrieved the ListOfOrders. So ideally I would like to check if there has been an addition to a tblOrder.tblItems collection and only newly retrieve that collection from the server if required.

The background is, that my model is a bit more complex: It consists of Orders, which consist of Parts, which consist of Tasks. So to assess the progress of each order I have to retrieve all parts that belong to an order and for each of them I have to see how many of the tasks have been completed. In a database with 200 job, each with 1 to 10 parts this simply makes my program to slow in terms of responsiveness ...

Can anyone help me?

Original Question

I found a lot of questions concerning DataContext, but I have not found a solution to my problem yet. If I do the following:

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

This gives me a list of the entities in the tblOrder table. But now I want to do this:

DataTable Orders = new DataTable();
Orders.Columns.Add("Order-ID", typeof(int));
Orders.Columns.Add("Order-Name", typeof(string));
Orders.Columns.Add("Order-Items", typeof(string));

dataGridView1.DataSource = Orders;

foreach (tblOrder Order in ListOfOrders)
{
    var newRow = Orders.NewRow();
    newRow["Order-ID"] = Order.orderID;
    newRow["Order-Name"] = Order.orderName;
    newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList());
        // System.ObjectDisposedException
    (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
}

And I can not because accessing all entities in the tblItem table that are related to orders by foreign key don't seem to be stored.

What which is working:

DataClasses1DataContext DataContext = new DataClasses1DataContext();
ListOfOrders = DataContext.tblOrder.ToList();

DataTable Orders = new DataTable();
Orders.Columns.Add("Order-ID", typeof(int));
Orders.Columns.Add("Order-Name", typeof(string));
Orders.Columns.Add("Order-Items", typeof(string));

dataGridView1.DataSource = Orders;

foreach (tblOrder Order in ListOfOrders)
{
    var newRow = Orders.NewRow();
    newRow["Order-ID"] = Order.orderID;
    newRow["Order-Name"] = Order.orderName;
    newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList()); 
    (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
}

DataContext.Dispose();

But as I understand this is not desirable.

EDIT

I extended my example into a Controller-View-Pattern.

using System.Collections.Generic;
using System.Linq;

namespace TestApplication
{
    class Controller
    {
        private List<tblOrder> _orders;
        public IList<tblOrder> Orders
        {
            get
            {
                return _orders;
            }
        }

        public Controller()
        {
            using (var DataContext = new DataClasses1DataContext())
            {
                _orders = DataContext.tblOrder.ToList();
            }
        }
    }
}

And the view now retrieves the Orders from the controller:

using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace TestApplication
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Controller controller = new Controller();

            DataTable Orders = new DataTable();
            Orders.Columns.Add("Order-ID", typeof(int));
            Orders.Columns.Add("Order-Name", typeof(string));
            Orders.Columns.Add("Order-Items", typeof(string));

            dataGridView1.DataSource = Orders;

            foreach (tblOrder Order in controller.Orders)
            {
                var newRow = Orders.NewRow();
                newRow["Order-ID"] = Order.orderID;
                newRow["Order-Name"] = Order.orderName;
                newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList());
                (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
            }
        }
    }
}

Sadly the problem remains the same ...

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70
Jonidas
  • 187
  • 4
  • 21
  • Calling Dispose manually instead of using a `using` block is not desirable. But you could trivially fix that. Just put in a using block. – nvoigt Aug 11 '15 at 10:19
  • That is correct and works for this example, but this becomes a problem if for example I have a controller that hadles a List of Orders that is accessed by a view, who displays that list. – Jonidas Aug 11 '15 at 10:27
  • You should not dispose context too quickly, instead you can keep context as a instance member of form and dispose context when form is disposed. – Akash Kava Aug 19 '15 at 06:34

4 Answers4

4

Entity Framework lazy-loads object data, meaning it loads the minimum amount of data it has to as late as possible. Take your query:

ListOfOrders = context.tblOrder.ToList();

Here you are requesting all of the records in the tblOrder table. Entity Framework doesn't read ahead in your program and understand that you will be looking at the tblItem table after the context has been disposed, so it assumes it can load the tblItem data later. Being lazy, it loads the bare minimum requested: The list of records in tblOrder.

There are two ways is a way around this:

Disable lazy loading

    using (var context = new DataClasses1DataContext())
    {
        data.Configuration.LazyLoadingEnabled = false;
        _orders = context.tblOrder.ToList();
    }

With LazyLoadingEnabled=false Entity Framework will select the entire contents of the tblOrder table and all tables connected with it via a foreign key. This may take a while and use a lot of memory, depending on the size and number of related tables.

(Edit: My mistake, disabling LazyLoading does not enable eager loading, and there is no default configuration for eager loading. Apologies for the misinformation. The .Include command below looks like the only way to go.)

Include additional tables

    using (var context = new DataClasses1DataContext())
    {
        data.Configuration.LazyLoadingEnabled = false;
        _orders = context.tblOrder.Include("tblItems").ToList();
    }

This tells Entity Framework to load all related data from tblItems up front while it's loading the tblOrders table data. EF still doesn't load any data from other related tables, so that other data will be unavailable after the context is disposed.

However, this does not solve the problem of stale data -- that is, over time the records in dataGridView1 will no longer be up-to-date. You could have a button or timer that triggers a refresh. The simplest method of refreshing would be to do the entire process over again -- reload _orders, then selectively repopulate dataGridView1.

Community
  • 1
  • 1
MikeTV
  • 645
  • 6
  • 14
  • 1
    in your first example context will be disposed – Backs Aug 14 '15 at 04:28
  • @Backs Yes, that's the idea. The context will be properly disposed and the data will be present in the list of objects. – MikeTV Aug 14 '15 at 14:02
  • @MikeTV While include does work I now want to turn of LazyLoading instead (I need to many "includes" otherwise). But If I do so, the 1:1 relations are null and the 1:Many relations are empty Lists. If I just include all relations it works. What am I doing wrong? – Jonidas Aug 20 '15 at 14:57
  • @Jonas Sorry, I misunderstood how the LazyLoadingEnabled property works in that case! `Include` looks like it is the only way to go. Thankfully you can chain them like `.Include("foo").Include("foo.bar")` Updated the answer to be more clear, with supporting links. – MikeTV Aug 21 '15 at 22:08
1

I advice you to create new result class, that contains your data:

class Result 
{
    int ID {get;set;}
    string OrderName {get;set;}
    IEnumerable<string> ItemNames {get;set;}
}

Select needed data:

class Controller
{
    private List<Result> _orders;
    public IList<Result> Orders
    {
        get
        {
            return _orders;
        }
    }

    public Controller()
    {
        using (var DataContext = new DataClasses1DataContext())
        {
            _orders = DataContext.tblOrder.Select(o=> 
                      new Result
                      {
                          ID = o.orderID,
                          OrderName = o.orderName,
                          ItemNames = o.tblItem.Select(item=> item.itemName)
                      }).ToList();
        }
    }
}

And after that bind this collection do grid view. So, you'll get all your data before disposing context and you have no more dependencies.

Backs
  • 24,430
  • 5
  • 58
  • 85
0

Just an answer to the first question.
Like EF says, you disposed the context after the [unit of] work (a must in ASP, a good practice in WinForm/WPF).

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

after this, if you try to run this statement

ListOfOrders.First().tblItems.toList();

EF works in this way:
- get the first element of ListOfOrders that is a proxy to your object.
- try to get the tblItems related with LazyLoad.
At this point, to retrieve the tblItems needs a database access with a context that is retrieved from the proxy of the ListOfOrder first element BUT the context is disposed.

So you can't use this approach.

There are 2 solutions and some variations on them:
- You can read all the tblItems before disposing the context (you can see it in another answer) but is a not scalable approach.
- When you need to access to tblItems you retrieve the Order from a new context and do what you need before disposing it. In your case

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
{
    int Id = ListOfOrders.First().Id;
    var myListOftblItems = DataContext.tblOrder.Find(Id).tblItems.ToList();
}

A variation on this approach is to read only tblItems. You can do this if tblItems has exposed also the foreign key field and not only the navigation property (usually I don't).

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
{
    int Id = ListOfOrders.First().Id;
    var myListOftblItems = DataContext.tblItems.Where(t => t.IdOrder == Id).ToList();
}
bubi
  • 6,414
  • 3
  • 28
  • 45
0

The default behavior of EF is to Lazy Load entities. In general this is a good idea and I think you should not disable it if you don't have very good reasons.

EF also provides methods for Eager Loading specified entities:

// Add this!
using System.Data.Entity;

Then you can use the Include method:

public static IList<Order> GetOrdersAndItems()
{
    List<Order> orders;

    using (var context = new ShopDC())
    {
        context.Database.Log = Console.WriteLine;

        Console.WriteLine("Orders: " + context.Orders.Count());

        orders = context.Orders
            .Where(o => o.OrderID > 0)
            // Tells EF to eager load all the items
            .Include(o => o.Items)
            .ToList();
    }

    return orders;
}

Now you can use GetOrdersAndItems to load a list with all your orders and each order will contain all the Items:

public static void Run()
{
    IList<Order> disconnectedOrders = GetOrdersAndItems();

    foreach (var order in disconnectedOrders)
    {
        Console.WriteLine(order.Name);

        foreach (var item in order.Items)
        {
            Console.WriteLine("--" + item.Name);
        }
    }
}

For more examples and multiple level includes take a look here

Note: using a helper or a controller or a repository is not important in the understanding of this mechanism, so I simply used a static method.

deramko
  • 2,807
  • 1
  • 18
  • 27