C# LINQ 展平父列表和两个嵌套的子列表并将其填充到 datagridview

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

我有这段代码,我想将其填充到 windows 表单 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; }

    }

我有这段代码可以正常工作,但我无法让它也显示电子邮件

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

dataGridView1.DataSource = result;

根据你的意思,它可以像下面这样实现:

var result = c.SelectMany(
    // Here we merge phone and email together, give null if no value for either.
    x => Enumerable.Range(0, new[] { x.Emails.Count, x.Telephone.Count }.Max())
        .Select(n => new { Phone = x.Telephone.ElementAtOrDefault(n), Email = x.Emails.ElementAtOrDefault(n) }), 
    // Select phone and email at the same row.
    (x, y) => new { x.ContactId, x.Contact.Nom, x.Contact.Prenom, y.Phone?.PhoneNumber, y.Email?.EmailAddress })
.ToList();

dataGridView1.DataSource = result;

一种方法是通过查询语法Email可枚举执行类似的SelectMany,然后加入 适用标准(例如,相同的联系人标识符)

var phoneNumbers = c.SelectMany(c => c.Telephone, (contact, phone) => new { contact.ContactId, contact.Contact.Nom, contact.Contact.Prenom, phone.PhoneNumber });
var result = from phone in phoneNumbers
             // return back join condition (e.g, ContactId) and email address
             from email in c.SelectMany(c => c.Emails, (contact, email) => new { contact.ContactId, email.EmailAddress } )
             where email.ContactId == phone.ContactId
             select new { phone.ContactId, phone.Nom, phone.Prenom, email.EmailAddress, phone.PhoneNumber };

emailGrid.DataSource = result.ToList();

List<myClass> 用作网格的 DataSource 的问题是网格不会显示作为集合的 myClass 的属性。就像 TelephoneEmails 列表一样。它将显示 Class,但是,它将默认并使用 classes ToString 方法并将其显示到“单个”列中。

在您的示例中,使用 Contacts Class 作为 List<Contacts> 作为 DataSource 到网格,我相信您知道 Person 对象正确显示,因为它是 Class,Phone 和电子邮件对象也没有显示,因为它们是对象的集合。

在此示例中,Person 对象可能显示为网格中的一列,但是,它会显示类似...“SolutionName.Person”的内容。如果 Person 有一个被覆盖的 ToString 方法,那么它将显示该方法 return 编辑的内容。但是,即使使用重写的 ToString 方法,它也不会将 Person 对象“拆分”为两列……一列用于 Nom,另一列用于 Prenom……您的代码需要这样做。

对于 phone 号码和电子邮件……不会为这些属性生成任何列,因为它们是“集合”属性并且网格不知道如何将“多个”项目放入单个细胞。这就是它们被忽略的原因。

此外,正如我评论的那样,“重复”x 行的 NomPrenom 数据似乎是一个糟糕的 UI 想法。 IMO,每个 Person 一行,如果该人有多个 phone 号码和电子邮件,则添加所有 phone 号码和电子邮件,使它们位于单个单元格中。这将删除冗余数据重复。

那么,鉴于此……我们如何“展平”数据,以便我们可以让 Person 属性显示在两个不同的列中,并将所有 phone 数字显示在一个单元格中以及单个单元格中的所有电子邮件地址?

下面是一个可能的解决方案。

首先,我建议您只需将这些属性“添加”到 Contacts class。在 Contact class 中,我们将向 class 添加属性,这只是 return PersonNomPrenom 属性.

这将解决在网格中将 NomPrenom 属性显示为两个不同列的问题。但是,我们仍然存在 PhoneNumbersEmailAddresses.

的“集合”问题

在这种情况下,一种解决方案是将 string 属性 添加到 Contacts class 并使其 return 成为带有 ALL 的单个字符串phone 数字和另一个 string 属性 return 所有 Emails 作为一个 string. 一旦完成,网格将显示这些属性,因为它是单个字符串而不是字符串的“集合”。

因此,Contacts class 中需要进行一些更改。以下是上述内容的示例。请注意,我将 class 名称更改为 Contact 而不是 Contacts,并将 Person 属性 名称更改为 ThePerson 而不是 Contact. 所以,你需要相应地调整你的代码。

更新后的 Contact class 可能看起来像……

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;
    }
  }
}

如前所述,我们添加了两个名为 NomPrenom 的属性,它们只是 return 来自 ThePerson 对象的那些属性。这将为这两个属性创建一个列。

接下来,我们添加了 PhoneNumbersEmailAddresses 属性。在这种情况下,我们需要遍历所有 phone 数字并将每个 phone 数字添加到一个字符串中。在这种情况下,每个 phone 数字将在一行中,因此我们需要调整网格行高以适应有多少行。否则,某些 phone 数字可能会在单元格显示中被截断。同样的想法也用于电子邮件地址。

接下来,我们需要手动将列添加到网格中。然后告诉网格不要自动生成列,因为我们显然已经添加了它们。每列中的键 属性 将是它的 DataPropertyName。 属性 告诉网格“哪个”属性 在给定 DataSource 的列中显示。在这种情况下,我们要将第一列 DataPropertyName 设置为 ContactID,将第二列设置为 Nom,将第三列设置为 Prenom,将第四列设置为 PhoneNumbers,最后最后到EmaillAddresses。为了帮助创建一个方法到 return 具有给定属性的列...类似于...

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 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;
}

上面我们将所有列配对到 Contacts class 中的 属性,然后设置单元格换行模式以容纳 PhoneNumersEmailAddresses属性。除了自动调整列宽大小以显示所有数据。

为了做一个完整的例子,下面的代码应该演示上面描述的内容。注意,我更改了一些 class 名称和 属性 名称,您需要调整代码以适合我的代码或更改我的代码以适合您的代码。

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;
}

根据 Enigmativity 的评论进行编辑。

正如@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;

我希望这有道理并有所帮助。