3

Hypothetical example: I have a list of customers and a list of products in memory. I want to be able to access the products that each customer has bought as well as the customers who bought a specific product:

class Customer
{
    public List<Product> Products { get; set; }
}
class Product
{
    public List<Customer> CustomersWhoBoughtThis { get; set; }
}


var customers = new List<Customer>();
var products = new List<Product>();

//add the customers
var customer1 = new Customer();
customers.Add(customer1);
var customer2 = new Customer();
customers.Add(customer2);

//add the products
var product1 = new Product();
products.Add(product1);
var product2 = new Product();
products.Add(product2);

//add a purchased product for customer1
customer1.Products.Add(product1);

//How do I get customer1 from product1?
foreach (var customerWhoBoughtProduct1 in product1.CustomersWhoBoughtThis)
{
    //do something
}

I could just manually add the customer to the product's CustomersWhoBoughtThis list, but I feel like this is prone to errors since you may forget to do it somewhere.

Is there a better implementation to this? Maybe some sort of wrapper class that handles the functionality, but I'm not sure how it would be implemented. Any advice?

Thanks

hofnarwillie
  • 3,563
  • 10
  • 49
  • 73
  • How would a hypothetical wrapper class do what you want without you needing to manually add the relation inside that class's code? There is no other way to track the relation. So the only thing left to decide is where exactly to put the code that adds. – Jon Nov 29 '13 at 12:04
  • Putting code used more than once inside a method in order to facilitate reuse is what they teach at programming kindergarten, I really don't think you needed to hear that from someone else in order to do so. As for more "engineered" solutions to do "it", the problem is that you don't specify what "it" is. In my opinion this question cannot be answered meaningfully because it is too generic. – Jon Nov 29 '13 at 12:38
  • That's certainly true for your own definition of "meaningful". Also, insulting people who point out that you would get much better answers if you improved the question might have some benefit but certainly not one that I can see. – Jon Nov 29 '13 at 12:53
  • Please also take into consideration that all the answers you got are quite different from one another. That might hint at something. – Jon Nov 29 '13 at 13:00
  • I already tried to do that far more than I should have. Cheers. – Jon Nov 29 '13 at 13:06
  • I said that there is no way to answer **meaningfully** and I certainly **did not** say that the answers are "too many", no idea why your interpretation differs from the facts like that. I also said that I have no desire to continue this conversation. – Jon Nov 29 '13 at 13:12
  • In this post, there is an elegant answer for this question http://stackoverflow.com/questions/1299920/how-to-handle-add-to-list-event – EngelbertCoder Nov 29 '13 at 12:05

6 Answers6

2

You can define some wrapper class, or implement your own IList interface, then you can implement your own event supporting notification. However it's much simpler to use built-in stuff. I would use a BindingList<T>, it has an event called ListChanged which allows you to get some notifications including item added notification. Try the following code:

class Customer {
  public Customer(){
    Products = new BindingList<Product>();
    Products.ListChanged += (s,e) => {
       if(e.ListChangedType == ListChangedType.ItemAdded){
         Products[e.NewIndex].CustomersWhoBoughtThis.Add(this);
       }
    };
  }
  public BindingList<Product> Products { get; set; }
}
King King
  • 61,710
  • 16
  • 105
  • 130
  • Thanks, I really like this method. Do you know if there is a significant performance hit with this over the use of a List? – hofnarwillie Nov 29 '13 at 13:05
  • @hofnarwillie Of course it will be not as fast as the `List` but it is not much difference, moreover if you create your own wrapper class, that approach is of course not as fast as a simple `List`, accessing the item is the same, only when adding, removing... there is a little difference in performance. This is also helpful when you want to track other operation (just look in the `ListChangedType`), when you bind your list to some control, this is the only convenient way to support tracking correctly. I believe that this approach is acceptable. – King King Nov 29 '13 at 13:11
2

Usually when I have a circular relationship, I designate one of the entities as the "owner" of the relationship. The owner is responsible for managing the relationship, so any code for that management, should be encapsulated within it.

In this case, since the customer actually does the action (A Customer buys a Product), it's best if the Customer class is the relationship owner.

So, the Product class will be:

public class Product
{
  //other product properties

  //field for the customers list
  private List<Customer> customers = new List<Customer>();

  //read-only property for outside access
  public IEnumerable<Customer> Customers 
  {
    get { return customers; }
  }

  //internal method that will only be called by the customer class
  internal void AddCustomer(Customer customer)
  {
    customers.Add(customer);
  }

  //internal method that will only be called by the customer class
  internal void RemoveCustomer(Customer customer)
  {
    customers.Remove(customer);
  }
}

and the Customer will be:

public class Customer
{
  //other customer properties

  //field for the product list
  private List<Product> products = new List<Product>();

  //read-only property for outside access
  public IEnumerable<Product> Products
  {
    get { return products; }
  }

  //public method, open to the world
  public void BuyProduct(Product product)
  {
    products.Add(product);
    product.AddCustomer(this);
  }

  //public method, open to the world
  public void RemoveProduct(Product product)
  {
    products.Remove(product);
    product.RemoveCustomer(this);
  }        
}

Now you just do:

customer1.BuyProduct(product1);

and the references are set correctly. Additionally, an outside user (from another assembly) will be unable to call product1.AddCustomer.

Also, it always important to note that serialization (to XML or to JSON) of circular references is a bitch, as the customer has a product that has a customer that has a product that has a stack overflow exception.

SWeko
  • 30,434
  • 10
  • 71
  • 106
1

Create a method that will do both operations

void PairCustomerWithProduct(Customer customer, Product product)
{
    customer.Products.Add(product);
    product.CustomersWhoBoughtThis.Add(customer);
}

And whenever you need to pair a product with a customer, call this method.

Ondrej Janacek
  • 12,486
  • 14
  • 59
  • 93
1

I will not expose List<T> as public, Instead return a array or sequence. Expose methods to add/remove product that's it.

class Customer
{
    private List<Product> Products { get; set; }
    public void AddProduct(Product product)
    {
       Products.Add(product);
       if(!product.CustomersWhoBoughtThis.Contains(this))
           product.CustomersWhoBoughtThis.Add(this);
    }

    public void RemoveProduct(Product product)
    {
       Products.Remove(product);
       if(product.CustomersWhoBoughtThis.Contains(this))
           product.CustomersWhoBoughtThis.Remove(this);
    }

    public IEnumerable<Product> GetProducts()
    {
       return Products;
    }
}
Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
0

You can try this way,

Class Customer{listCustomer>} 

- Holds Customer Info Class Product {list} - Holds Product Info

Add one more class to maintain the combination

Class Sale{ public Customer, public Product }

One more class to have the list of SoldProducts

Class SoldProduct{ list<Sale> }
David Pilkington
  • 13,528
  • 3
  • 41
  • 73
Devivek
  • 11
  • 1
0

Do a relationship class.

Note: I haven't included error handling, just the basic idea. Note also that the Relationship class (for a one to many relationship) can easily be made generic and be reused for any kind of relationship. I left it concrete to make it more clear.

class Customer
{
  public List<Products> Products 
  { 
     get { return CustomerProductRelationship.CustomerProducts[this];}
     set 
     {
        CustomerProductRelationship.AddPair(value, this);
     }
}

class Product
{
  public List<Products> Products 
  { 
     get { return CustomerProductRelationship.ProductCustomers[this];}
     set 
     {
        CustomerProductRelationship.AddPair(this, value);
     }
  }
}

static class CustomerProductRelationship
{
  public static Dictionary<Customer, List<Product>> CustomerProducts;
  public static Dictionary<Product, List<Customer> ProductCustomers;

  public static AddPair(Product product, Customer customer)
  {
       if (!CustomerProducts.Contains(customer))
          CustomerProducts.Add(customer, new List<Product>);

       CustomerProducts[customer].Add(product);

       if (!ProductCustomers.Contains(product))
          ProductCustomers.Add(product, new List<Customers>);

       CustomerProducts[product].Add(customer);
   }
}
Jorge Córdoba
  • 51,063
  • 11
  • 80
  • 130