Search code examples
c#.netlinq-to-entities

C# LINQ to flatten parent list and two nested children lists and populate it to datagridview


I have this code that I woud like to populate to a datagridview in windows form c#

   public class Person
   {
       public string Nom { get; set; }
       public string Prenom { get; set; }
   }

    public class Phone
    {
        public int PhoneId { get; set; }
        public string PhoneNumber { get; set; }
    }

    public class Email
    {
        public int EmailId { get; set; }
        public string EmailAddress { get; set; }
    }

    public class Contacts
    {
        public int ContactId { get; set; }
        public Person Contact { get; set; }
        public List<Phone> Telephone { get; set; } 
        public List<Email> Emails { get; set; }

    }

I have this code that works fine but I cannot get it to work to show the emails as well

    var result = c.SelectMany(x => x.Telephone, (x, y) => 
new { x.ContactId, x.Contact.Nom, x.Contact.Prenom, y.PhoneNumber }).ToList();

dataGridView1.DataSource = result;

Here is an image of what I have


Solution

  • The issue with using a List<myClass> as a DataSource to the grid is that the grid will not display properties of myClass that are collections. Like the Telephone and Emails lists. It will display a Class, however, it will default and use the classes ToString method and display that into a "single" column.

    In your example, using the Contacts Class as a List<Contacts> as a DataSource to the grid, I am sure you are aware that neither the Person object is displayed properly since it is a Class nor is the Phone and Email objects displayed because they are collections of objects.

    In this example, the Person object may display as a column in the grid, however, it will display something like… “SolutionName.Person.” If Person had an overridden ToString method then it would display what that method returned. However, even with an overridden ToString method, it will NOT “split” the Person object into two columns… one for Nom and the other Prenom… your code would need to do this.

    In the case of the phone numbers and emails… no column will be generated for those properties because they are “collection” properties and the grid does not know how to put “multiple” items into a single cell. This is why they are ignored.

    In addition, as I commented, it seems like a poor UI idea to “repeat” the Nom and Prenom data for x number of rows. IMO, one row for each Person and if that person has multiple phone numbers and emails, then add ALL the phone numbers and emails such that they will be in a SINGLE cell. This will remove the redundant data duplication.

    So, given this… how can we “flatten” the data so we can get both Person properties to display in two different columns and also have ALL the phone numbers in a single cell and ALL the email addresses in a single cell?

    One possible solution follows.

    First, I suggest you simply “add” these properties to the Contacts class. In the Contact class we will add the properties to the class that simply return the Persons Nom and Prenom properties.

    This will solve the issue of displaying the Nom and Prenom properties as two different columns in the grid. However, we still have the “collection” problem with the PhoneNumbers and the EmailAddresses.

    In this case, one solution is to add a string property to the Contacts class and have it return a single string with ALL the phone numbers and another string property that returns all the Emails as a single string. Once this is done, then the grid WILL display those properties since it is single string and not a “collection” of strings.

    So, some changes are needed in the Contacts class. Below is an example of what is described above. Note, I CHANGED the class name to Contact instead of Contacts and I CHANGED the Person property name to ThePerson instead of Contact. So, you need to adjust your code accordingly.

    This updated Contact class may look something like…

    public class Contact {
      public int ContactId { get; set; }
      public Person ThePerson { get; set; }
      public List<Phone> Telephone { get; set; }
      public List<Email> Emails { get; set; }
    
      public string PhoneNumbers {
        get {
          if (Telephone == null) {
            return "";
          }
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < Telephone.Count; i++) {
            sb.Append(Telephone[i].PhoneNumber.ToString());
            if (i < Telephone.Count - 1) {
              sb.AppendLine();
            }
          }
          return sb.ToString();
        }
      }
    
      public string EmailAddresses {
        get {
          if (Emails == null) {
            return "";
          }
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < Emails.Count; i++) {
            sb.Append(Emails[i].EmailAddress.ToString());
            if (i < Emails.Count - 1) {
              sb.AppendLine();
            }
          }
          return sb.ToString();
        }
      }
    
      public string Nom {
        get {
          if (ThePerson == null) {
            return "";
          }
          return ThePerson.Nom;
        }
      }
    
      public string Prenom {
        get {
          if (ThePerson == null) {
            return "";
          }
          return ThePerson.Prenom;
        }
      }
    }
    

    As described previously, we added two properties called Nom and Prenom and they simply return those properties from the ThePerson object. This will make a column for both properties.

    Next, we have the added PhoneNumbers and EmailAddresses properties. In this case, we need to loop through all the phone numbers and add each phone number to a single string. In this case, each phone number will be on a single line, so we will need to adjust the grids row heights to accommodate however many lines there are. Otherwise, some phone numbers may be cut off in the cells display. This same idea is used for the Email addresses.

    Next, we need to manually add the columns to the grid. Then tell the grid to not auto generate the columns since we obviously have already added them. The key property in each column will be its DataPropertyName. This property tells the grid “which” property to display in the column given a DataSource. In this case, we want to set the first columns DataPropertyName to ContactID, the second column to Nom, third to Prenom, fourth to PhoneNumbers and finally the last to EmaillAddresses. To help a method is created to return a column with the given properties… Something like…

    private DataGridViewColumn GetDGVCol(string colName, string dataPropertyName, string headerText) {
      DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn();
      col.Name = colName;
      col.DataPropertyName = dataPropertyName;
      col.HeaderText = headerText;
      return col;
    }
    

    To use the above helper in this example, I created an additional method that adds the columns to the grid as described.

    private void AddDGV_Columns() {
      dataGridView1.Columns.Add(GetDGVCol("ContactID", "ContactID", "ContactID"));
      dataGridView1.Columns.Add(GetDGVCol("Nom", "Nom", "Nom"));
      dataGridView1.Columns.Add(GetDGVCol("Prenom", "Prenom", "Prenom"));
      dataGridView1.Columns.Add(GetDGVCol("PhoneNumbers", "PhoneNumbers", "PhoneNumbers"));
      dataGridView1.Columns.Add(GetDGVCol("EmailAddresses", "EmailAddresses", "EmailAddresses"));
      dataGridView1.DefaultCellStyle.WrapMode = DataGridViewTriState.True;
      dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
    }
    

    Above we mate all the columns to a property in the Contacts class, then set the cells wrap mode to accommodate multiple lines in the PhoneNumers and EmailAddresses properties. In addition to auto sizing the column width to show all the data.

    To make a complete example, the code below should demonstrate what is described above. NOTE, I have changed some class names and property names and you will need to adjust your code to fit mine or change my code to fit yours.

    enter image description here

    List<Contact> Contacts;
    
    public Form1() {
      InitializeComponent();
    }
    
    private void Form1_Load(object sender, EventArgs e) {
      Contacts = GetContacts();
      AddDGV_Columns();
      dataGridView1.AutoGenerateColumns = false;
      dataGridView1.DataSource = Contacts;
    }
    
    private void AddDGV_Columns() {
      dataGridView1.Columns.Add(GetDGVCol("ContactID", "ContactID", "ContactID"));
      dataGridView1.Columns.Add(GetDGVCol("Nom", "Nom", "Nom"));
      dataGridView1.Columns.Add(GetDGVCol("Prenom", "Prenom", "Prenom"));
      dataGridView1.Columns.Add(GetDGVCol("PhoneNumbers", "PhoneNumbers", "PhoneNumbers"));
      dataGridView1.Columns.Add(GetDGVCol("EmailAddresses", "EmailAddresses", "EmailAddresses"));
      dataGridView1.DefaultCellStyle.WrapMode = DataGridViewTriState.True;
      dataGridView1.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
      dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
    }
    
    private DataGridViewColumn GetDGVCol(string colName, string dataPropertyName, string headerText) {
      DataGridViewTextBoxColumn col = new DataGridViewTextBoxColumn();
      col.Name = colName;
      col.DataPropertyName = dataPropertyName;
      col.HeaderText = headerText;
      return col;
    }
    
    
    
    private List<Contact> GetContacts() {
      List<Contact> Contacts = new List<Contact>();
      List<Phone> PhoneNumbers = new List<Phone>();
      Phone pn = new Phone { PhoneId = 1, PhoneNumber = "111-11-11" };
      PhoneNumbers.Add(pn);
      pn = new Phone { PhoneId = 2, PhoneNumber = "222-22-22" };
      PhoneNumbers.Add(pn);
      pn = new Phone { PhoneId = 3, PhoneNumber = "222-22-22" };
      PhoneNumbers.Add(pn);
      Person = new Person { Nom = "Jeff", Prenom = "Prosper" };
      List<Email> emails = new List<Email>();
      Email em = new Email { EmailId = 1, EmailAddress = "email1@someISP.com" };
      emails.Add(em);
      em = new Email { EmailId = 2, EmailAddress = "email2@someISP.com" };
      emails.Add(em);
      Contact newC = new Contact { ContactId = 1, Telephone = PhoneNumbers, Emails = emails, ThePerson = person };
      Contacts.Add(newC);
      person = new Person { Nom = "Jimmy", Prenom = "Gean" };
      PhoneNumbers = new List<Phone>();
      pn = new Phone { PhoneId = 1, PhoneNumber = "333-33-33" };
      PhoneNumbers.Add(pn);
      emails = new List<Email>();
      em = new Email { EmailId = 1, EmailAddress = "emailXX1@someISP.com" };
      emails.Add(em);
      em = new Email { EmailId = 2, EmailAddress = "emailXX2@someISP.com" };
      emails.Add(em);
      newC = new Contact { ContactId = 2, Telephone = PhoneNumbers, Emails = emails, ThePerson = person };
      Contacts.Add(newC);
      return Contacts;
    }
    

    Edit as per Enigmativity's comment.

    As @Enigmativity notes a more succinct approach could be used like below. You would use these properties below as opposed to the above properties. Thanks, Enigmativity 😉

    public string PhoneNumbers => Telephone == null ? "" : String.Join(Environment.NewLine, Telephone.Select(x => x.PhoneNumber));
    
    public string EmailAddresses => Emails == null ? "" : String.Join(Environment.NewLine, Emails.Select(x => x.EmailAddress));
    
    public string Nom => ThePerson == null ? "" : ThePerson.Nom;
    
    public string Prenom => ThePerson == null ? "" : ThePerson.Prenom;
    

    I hope this makes sense and helps.