1

So I currently have two objects.

Object A

public class Person
{

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Pet MyPet  { get; set; }
}

and Object B

public class Pet
{
    public string Name { get; set; }
    public string Animal  { get; set; }
}

So then I created 2 Person objects and 2 Pet objects.

Pet myDog = new Pet("Fluffy", "Dog");
Person A = new Person("John", "Smith", myDog);
Pet myCat = new Pet("Furball", "Cat");
Person B = new Person("Jane", "Doe", myCat);

Then I added Person A and Person B into a List of Person objects, and made the list the data source for my data grid.

The results were not what wanted. The data grid auto populated the columns as FirstName, LastName, and MyPet, instead of also including Name and Animal. Also, while the FirstName and LastName columns properly displayed the names of both people, MyPet showed TestDemo.Pet, neither object name, Name, or Animal.

So I did some digging and used the designer to add in the columns themselves and making their DataPropertyName corresponding to how they were named in the class. While they did fill up correctly for the Person object (I even switched the positions of their first names and last name and it works), the columns for the pets did not work.

So I am wondering why is that the case?

As an aside, is there also a way to make the columns sort-able?

1
  • 1
    The Columns of a DataGridView can only be bound to the first-level properties of a class. If you want to show the pet's name and other attributes in a single, read-only, Column of your DGV, see the notes about the IFormattable implementation method here: DataGridView Cell with an Interface Type doesn't show a Value. It may apply to your case, too. Otherwise, you need a custom TypeDescriptor + TypeDescriptorProvider. You can also move the data to another type of container, which deconstructs the Pet object into its components for the binding Commented Mar 6, 2024 at 7:15

2 Answers 2

1

The simplest way is to add two properties in the Person class:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [Browsable(false)]
    public Pet MyPet { get; set; }

    public string PetName => MyPet.Name;
    public string PetAnimal => MyPet.Animal;
}

You can use BrowsableAttribute to hide the MyPet property.

To make the columns sort-able, check this question: How to enable DataGridView sorting when user clicks on the column header?

Sign up to request clarification or add additional context in comments.

Comments

1

Model - View

In modern programming, there is a tendency to separate your data (= Model) from how this data is displayed (= View). This has the advantages that you can reuse your model if you want to display it differently. Think of spreadsheet data where you want to display the data in rows and columns, or display the same data in a graph. Another advantage is that you can unit test your model without having to use the view part. It is possible to show others your view, without the actual data.

So there are numerous advantages to separating the model from the view. To adapt your model for your specific view, a special adapter class is needed: the ViewModel. Together these three classes are abbreviated as MVVM. If you are not familiar with this concept, consider to do some background reading about this.

MVVM and my question

So you already have classes Person and Pet. You want to display a sequence of Persons, each Person with his Pet.

By the way, are you certain that every Person has zero or one Pet? No person has more than one Pet? No Pet is owned by several member of a family? Let's keep it easy, and assume that every Person has zero or one Pet.

The Model

class Pet
{
    public string Name {get; set;}
    public string Animal {get; set;}

    ... // other properties
}

public Person
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public Pet Pet {get; set;}

    ... // other properties
}

You also have a method to fetch the Persons that you want to display:

IEnumerable<Person> GetPersonsToDisplay(...) {...}

So, that's your Model. You can Unit test this without the DataGridView. You can internally change the Pet and the Person, users of Pet and Person won't have to change as long as you don't change the interface. If it's likely that your internal data changes, consider using interfaces. But that's out of scope of the question.

the View (part 1)

You want to display this model in a DataGridView with several columns: From the Person you want to display his FirstName and LastName, from his Pet you want to display the Name and the Animal.

Using visual studio designer, you added a DataGridView to your form and you added four columns to the DataGridView.

DataGridView dgvPersons = ...
DataGridViewColumn columnFirstName = ...
DataGridViewColumn columnLastName = ...
DataGridViewColumn columnPetName = ...
DataGridViewColumn columnAnimalType = ...

the ViewModel

The ViewModel connects your Model to your View.

interface : IDisplayedPerson
{
    string FirstName {get; set;}
    string LastName {get; set;}
    string PetName {get; set;}
    string PetType {get; set;}

}

I decided to create an interface, so if you later decide to fetch your data in a different way, for instance from a database, or from a CSV file, or from the internet, you won't have to change the interface, nor the classes that use the interface.

The interface has "set" methods, so the operator will be able to edit the displayed data. If you don't need this, remove the set. This will make most methods smaller.

class DisplayedPerson : IDisplayedPerson
{
    // Data from the Model:
    public Person Person {get; set;}

    // conversion from Model to View
    public string FirstName
    {
        get => this.Person.FirstName;
        set => this.Person.FirstName = value;
    }

    public string LastName
    {
        get => this.Person.LastName;
        set => this.Persons.LastName = value;
    }

    public string PetName 
    {
        get => this.Person.MyPet.Name;
        set => this.Person.MyPet.Name = value;
    }

    public string PetType
    {
        get => this.Person.MyPet.Animal;
        set => this.Person.MyPet.Animal = value;
    }
}

As your problem is very small, you don't really need to create a ViewModel class, you can easily directly display the sequence of Persons. However, this ViewModel class has the advantage, that if in future your model changes you don't have to change your View, nor the ViewModel interface. For example, if in future you have Persons with different properties, like a MiddleName, and you want to display this middle name not as a separate column, but in front of the LastName. Or maybe your Persons and Pets are fetched from a database as separate items, connected by a foreign key. In these cases you don't have to change the View, nor the interface, all you have to do is change the adapter between the changed Model and the unchanged View: the ViewModel.

So to fetch your Model data and your ViewModel data you need two methods, the first one I already mentioned above:

// Get the Model data:
IEnumerable<Person> GetPersons(...) {...}

The second one converts the Model data into the View data.

IEnumerable<IDisplayedPerson> PersonsToDisplay => this.GetPersons(...)
    .Select(person => new DisplayedPerson {Person = person;}

The latter method is usually a small method. Creating a separate method for this makes it easier to change it, for instance, suppose you only want to show Persons that have Dogs and you want to show them in alphabetical order:

IEnumerable<IDisplayedPerson> PersonsToDisplay => this.GetPersons(...)
    .Where(person => person.Pet != null
                  && person.Pet.Animal == "Dog")
    .OrderBy(person => person.LastName);

Note: again a small method, no change in your Model, nor in your DataGridView

Back to the View

In the constructor of your Form you define which column will show which property.

public MyForm()
{
    InitializeComponent();

    // define which column will show which property
    this.columnFirstName.DataPropertyName = nameof(IDisplayedPerson.FirstName);
    this.columnLastName.DataPropertyName = nameof(IDisplayedPerson.LastName);
    this.columnPetName.DataPropertyName = nameof(IDisplayedPerson.PetName);
    this.columnAnimalType.DataPropertyName = nameof(IDisplayedPerson.PetType);
}

You can also use Visual Studio designer to do this. The advantage of the constructor method is, that if you change the name of one of the properties, and you forget to update the DataPropertyName, the compiler will complain. Furthermore, for others it is easy to understand which column will show which property, no need to scan method InitializeComponent for this.

If you decide not to create a ViewModel interface, code would be like this:

this.columnLastName.DataPropertyName = nameof(IDisplayedPerson.LastName); this.columnPetName.DataPropertyName = nameof(IDisplayedPerson.Pet.Name);

The method to display the Persons is also small:

void DisplayPersons()
{
    this.dgvPersons.DataSource = this.PersonsToDisplay.ToList();
}

And presto! Your Persons and their Pets are shown in the DataGridView.

Alas, if you do it this way, the operator can't edit the Persons. If that is a requirement, you need to put the data in a BindingList<IDisplayedPerson>

BindingList<IDisplayedPerson> DisplayedPersons
{
    get => (BindingList<IDisplayedPerson>) this.dgvPersons.DataSource;
    set => this.dgvPersons.DataSource = value;
}

Filling the datagridview is a one liner:

void DisplayPersons()
{
    this.DisplayedPersons = new BindingList<IDisplayedPerson>(this.PersonsToDisplay.ToList());
}

When the operator has finished editing the data, he can indicate this by pressing for instance the Apply Now button:

void OnButtonApplyNow_Clicked(object sender, ...)
{
    this.ProcessEditedPersons();
}

void ProcessEditedPersons()
{
    // fetch the edited persons:
    ICollection<IDisplayedPerson> actualPersons = this.DisplayedPersons;

    // fetch the original persons: TODO: create this property
    ICollection<IDisplayedPerson> originalPersons = this.OriginalPersons;

    IEnumerable<IDisplayedPerson> removedPersons = originalPersons
        .Except(actualPersons);
    IEnumerable<IDisplayedPerson> addedPersons = actualPersons
        .Except(originalPersons);

    this.ProcessRemovedPersons(removedPersons);
    this.ProcessAddedPersons(addedPersons);
}

You should also find a way to process the changed persons. However, for this you need a method to identify the Persons, like a property Id. Otherwise, how will you be able to identify a person with a changed First name / last name / pet?

Some handy methods

To access the current person, or all selected persons:

IDisplayedPerson CurrentPerson => (IDisplayedPerson)this.dgvPersons.CurrentRow?.DataBoundItem);

IEnumerable<IDisplayedPerson> SelectedPersons = this.dgvPersons.SelectedRows
    .Cast<DataGridViewRow>()
    .Select(row => row.DataBoundItem)
    .Cast<IDisplayedPerson>();

Summary

Because you separated your model from your view, and created properties to access the model data as well as the view data, it is easy to display the Persons that you need to display, and to fetch and process the displayed data. These methods are usually very small, seldom more than one line. Therefore they are easy to understand, easy to change and maintain.

Because you separated your model, preferably in separate classes, it is easy to unit test the model, easy to reuse the model, and easy to change, without having to change the view.

Because you separated your view, it is easy to fill your view with different model data, for instance to be able to develop the model without the actual view.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.