wondering if there is an efficient way to shorten this code - c#

So my program is going to end up being fairly large and I don't want a whole lot of code that could just be shortened. Here is one instance I am looking for some tips on:
private void bookComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
string books = null;
// sets books to the clicked item
books = bookComboBox.SelectedItem.ToString();
selectedPictureBox.Visible = true;
// Loads string to list box and image to selectedPictureBox when programming is selected
if (books == "Programming")
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile("programming.png");
bookListBox.Items.Add("Visual Basic");
bookListBox.Items.Add("Java");
bookListBox.Items.Add("C#");
}
// Loads string to list box and image to selectedPictureBox when Networking is selected
else if (books == "Networking")
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile("networking.png");
bookListBox.Items.Add("LAN Networks");
bookListBox.Items.Add("Windows Networking");
bookListBox.Items.Add("More About Networking");
}
// Loads string to list box and image to selectedPictureBox when Web is selected
else if (books == "Web")
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile("html.png");
bookListBox.Items.Add("Web Programming");
bookListBox.Items.Add("JavaScript");
bookListBox.Items.Add("ASP");
}
}
The code works fine but I was just hoping to get some tips on shortening this code if necessary, any input is appreciated.

Assuming you can use C# 7's new Tuples:
private Dictionary<string, (string image, List<string> books)> books = new Dictionary<string, (string image, List<string> books)>
{
{ "Programming", ("programming.png", new List<string> { "Visual Basic", "Java", "C#"} ) },
{ "Networking", ("networking.png", new List<string> {"LAN Networks", "Windows Networking", "More About Networking"}) },
{ "Web", ("html.png", new List<string> {"Web Programming", "Javascript", "ASP"}) }
};
private void bookComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
// sets books to the clicked item
string book = bookComboBox.SelectedItem.ToString();
selectedPictureBox.Visible = true;
if (books.Keys.Contains(book))
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile(books[book].image);
foreach(var b in books[book].books)
{
bookListBox.Items.Add(b);
}
}
}
But a class is likely even better:
public class BookGroup
{
public string ImagePath {get;set;}
public List<string> Books {get;}
public BookGroup(string imagePath, params string[] books)
{
ImagePath = imagePath;
Books = new List<string>(books.Length);
Books.AddRange(books);
}
}
Which isn't all that different to use right now, but it formalizes some things that might make this code easier to work with down the road, and it's possible if you can't use Tuples yet. I might also have a Book class by itself, just for fun, but for now:
private Dictionary<string, BookGroup> books = new Dictionary<string, BookGroup>
{
{ "Programming", new BookGroup("programming.png", "Visual Basic", "Java", "C#")},
{ "Networking", new BookGroup("networking.png","LAN Networks", "Windows Networking", "More About Networking") },
{ "Web", new BookGroup("html.png", "Web Programming", "Javascript", "ASP") }
};
private void bookComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
// sets books to the clicked item
string book = bookComboBox.SelectedItem.ToString();
selectedPictureBox.Visible = true;
if (books.Keys.Contains(book))
{
bookListBox.Items.Clear();
BookGroup bg = books[book];
selectedPictureBox.Image = Image.FromFile(bg.ImagePath);
foreach(var b in bg.Books)
{
bookListBox.Items.Add(b);
}
}
}
Regardless, I'd definitely have a way to load these from a text file... likely a csv, or maybe even a small in-process database, so that I could update this listing without having to recompile or distribute new program code. And, with that in mind, in order to fit this data in a single structure in a single file, I'd likely also repeat the image and type with each book, so that my csv data looks like this:
Topic,Image,Title
Programming,programming.png,"Visual Basic"
Programming,programming.png,"Java"
Programming,programming.png,"C#"
Networking,networking.png,"LAN Networks"
Networking,networking.png,"Windows Networking"
Networking,networking.png,"More About Networking"
Web,html.png,"Web Programming"
Web,html.png,"Javascript"
Web,html.png,"ASP"
That changes the whole character of the code. I'm a bit biased, but I'd likely use this CSV Parser, and again assuming Tuples I'd produce something like this:
private List<(string Topic, string ImagePath, string Title)> books;
//In the form load code:
books = EasyCSV.FromFile("bookData.csv").Select(b => (b[0], b[1], b[2])).ToList();
//and finally, in the original selectindexchanged method:
private void bookComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
string topic = bookComboBox.SelectedItem.ToString();
selectedPictureBox.Visible = true;
var items = books.Where(b => b.Topic == topic).ToArray();
if(items.Length > 0)
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile(items[0].ImagePath);
bookListBox.Items.AddRange(items);
}
}

Make objects and use databindings.
public class Book
{
public BookType BookType { get; set; }
public string Name { get; set; }
public string Image { get; set; }
}
public enum BookType
{
Programming,
Networking,
Web,
}
public partial class Form1 : Form
{
private readonly List<Book> _books = new List<Book>
{
new Book { Image = "programming.png", BookType = BookType.Programming, Name = "VB" },
new Book { Image = "programming.png", BookType = BookType.Programming, Name = "Java" },
new Book { Image = "programming.png", BookType = BookType.Programming, Name = "C#" },
new Book { Image = "networking.png", BookType = BookType.Networking, Name = "LAN Networks" },
new Book { Image = "networking.png", BookType = BookType.Networking, Name = "Windows Networking" },
new Book { Image = "networking.png", BookType = BookType.Networking, Name = "More About Networking" },
new Book { Image = "html.png", BookType = BookType.Web, Name = "Web Programming" },
new Book { Image = "html.png", BookType = BookType.Web, Name = "Javascript" },
new Book { Image = "html.png", BookType = BookType.Web, Name = "ASP" },
};
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
var bookTypes = _books.GroupBy(b => b.BookType).Select(g => g.Key).ToList();
this.cboBookTypes.DataSource = bookTypes;
}
private void cboBookTypes_SelectedIndexChanged(object sender, EventArgs e)
{
var bookType = (BookType)this.cboBookTypes.SelectedItem;
var books = _books.Where(b => b.BookType == bookType).ToList();
var img = books.First().Image;
this.imgBook.Image = Image.FromFile(img);
this.lstBooks.DataSource = books;
this.lstBooks.DisplayMember = "Name";
}
}

If you are talking about the length of the code, I would suggest using switch-case-break-default construct
Switch the books variable.
This wont improve the performance though

I think you should create a class that represents book category. Then, you could simply iterate through all the category lists and extract the necessary information, like this:
string books = null;
books = bookComboBox.SelectedItem.ToString();
selectedPictureBox.Visible = true;
for (int i = 0; i < categories.Count; i++) {
if (books == categories[i].Name) {
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile(categories[i].ImagePath);
for (int j = 0; j < categories[i].Items.Count; j++) {
bookListBox.Items.Add(categories[i].Items[j]);
}
}
}

I would suggest to keep all the data in a configuration object and then iterate through that data when performing checks and assignments.
I would create a separate class to hold data for each book: name, picture file name and check box items string array.
Then I would create a list of that object and assign all the data manually on form initialization.
After that, in SelectedIndexChanged event handler, I would iterate (for loop) on each item and check if the book name matched. If it did, then I would use data from that object and then break; the loop.

I do not have visual studio, so giving you the points/suggestions to improve on.
switch should be preferred over if-elseif.
bookListBox.Items.Clear(); and selectedPictureBox.Image out of if block. Use a variable to set the image file name.

Create a class to represent a book list:
public class BookList
{
public string ImageName { get; set; }
public List<string> Items { get;set; }
}
Then create a dictionary to hold these items:
Dictionary<string, BookList> bookLists = new Dictionary<string, BookList>
{
{
"Programming",
new BookList { ImageName = "programming.png", Items = new List<string> { ... } }
}
...
};
Then modify your if statements to:
if (bookLists.ContainsKey(books))
{
bookListBox.Items.Clear();
selectedPictureBox.Image = Image.FromFile(bookLists[books].ImageName);
foreach (var b in bookLists[books].Items)
{
bookListBox.Items.Add(b);
}
}

Related

New list updates original one

Here i am using this code for copy one list to another
public class Person
{
public string Name { get; set; }
}
private void Form1_Load(object sender, EventArgs e)
{
var originalList = new List<Person>();
originalList.Add(new Person { Name = "name 1" });
originalList.Add(new Person { Name = "name 2" });
// var newList = originalList.ToList();
var newList = new List<Person>(originalList);
newList[0].Name = "New name";
Console.WriteLine(originalList[0].Name);
}
My result in console is 'New name', why this is happen? When i am updating my new list it also updates my original one. How can i fix this?
Do not worry, this is normal behavior, you have the original list, then you have another list filled in by the original, in your case, both lists point to the same items, which means that you have two references that point to the same memeory case , reason why you change an element from the original one the same element change in the second one and vice-versa .
Case 1 :
public class Person
{
public string Name { get; set; }
}
private void Form1_Load(object sender, EventArgs e)
{
//here you have the original list, you create your list
//you add element to your list .
var originalList = new List<Person>();
originalList.Add(new Person { Name = "name 1" });
originalList.Add(new Person { Name = "name 2" });
// you create a second list , but here the contain the
// same element than the original list
var newList = new List<Person>(originalList);
newList[0].Name = "New name";
Console.WriteLine(originalList[0].Name);
}

Display all values stored in a column to a label?

I have a table called T_Score and the column called Team1, it has some stored values and I want these values to be added and displayed on a label.
This is the code which stores the values in the table:
private void Btn_LeaderB_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new LeaderBoard());
SqlConnection conne = new SqlConnection(#"Data Source=LAPTOP-S2J1U9SJ\SQLEXPRESS;Initial Catalog=Unit4_IT;Integrated Security=True");
conne.Open();
string insertQuery = "insert into T_Score(Team1) " +
"values(#Team1)";
SqlCommand com = new SqlCommand(insertQuery, conne);
com.Parameters.AddWithValue("#Team1", txt_score4_tm1.Text);
com.ExecuteNonQuery();
conne.Close();
}
This code stores the value that needs to be added with the previous value.
Use the following query select sum(Team1) from T_Score; and instead of cmd.ExecuteNonQuery(); use cmd.ExecuteScalar(); and use the return value of the method ExecuteScalar
https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.executescalar
Like this:
private void WriteToLabel()
{
using (SqlConnection conne = new SqlConnection(#"Data Source=LAPTOP-S2J1U9SJ\SQLEXPRESS;Initial Catalog=Unit4_IT;Integrated Security=True"))
{
conne.Open();
string selectQuery = "select sum(Team1) from T_Score;";
using (SqlCommand com = new SqlCommand(selectQuery, conne))
label1.Content = Convert.ToString(com.ExecuteScalar());
}
}
The line
label1.Content = Convert.ToString(com.ExecuteScalar());
Will require a Label called label1 for it to work.
It tries to write the result of Convert.ToString(com.ExecuteScalar()) into the Content property of said Label.
You can also use a TextBlock and than use the Text Property, so this line:
textBlock1.Text = Convert.ToString(com.ExecuteScalar());
Notice instead of calling conne.Close(); manually, I wrapped the SqlConnection conne into a using Statement, this should always be done with objects that inherit from IDisposable.
You can read about IDisposable and the using statement here:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement
https://learn.microsoft.com/en-us/dotnet/api/system.idisposable
https://stackoverflow.com/a/61131/2598770
If you have the values already in a IEnumerable object eg. List<int> than you can just do the following
With a simple loop:
var myValues = new List<int>() { 1, 2, 3, 4, 5, 6 }; //<- list with temporary data
var sumOfValue = 0;
foreach (var myValue in myValues)
sumOfValue += myValue;
With LINQ:
var myValues = new List<int>() { 1, 2, 3, 4, 5, 6 }; //<- list with temporary data
var sumOfValue = myValues.Sum();
If you don't have an IEnumerable of int but instead a class than your code will look like this
public class MyValue
{
public int Value { get; set; }
//other properties
}
var myValues = new List<MyValue>()
{
new MyValue() { Value = 1 },
new MyValue() { Value = 2 },
new MyValue() { Value = 3 },
new MyValue() { Value = 4 },
new MyValue() { Value = 5 },
new MyValue() { Value = 6 },
}; //<- list with temporary data
With a simple loop:
var sumOfValue = 0;
foreach (var myValue in myValues)
sumOfValue += myValue.Value;
With LINQ:
var sumOfValue = myValues.Sum(x => x.Value);
In all those cases you will have to write sumOfValue into your Label or TextBlock like this
//Label
label1.Content = sumOfValue.ToString();
//or TextBlock
textBlock1.Text = sumOfValue.ToString();
To pass a value to a different page, all you would need to do is the following.
Search for your page in code behind eg. LeaderBoard.
It should look something like this
public partial class LeaderBoard : Page
{
//stuff...
}
Add a new Property to this class
public partial class LeaderBoard : Page
{
public int MyProperty { get; set; }
//stuff...
}
When you initialize the LeaderBoard eg. here
this.NavigationService.Navigate(new LeaderBoard());
Change the initialization to this:
this.NavigationService.Navigate(new LeaderBoard() { MyProperty = 7187, });
7187 is a random number this needs to be filled with what ever you need.
With this you have "transfered" data to LeaderBoard and the new property with its value can be accessed in LeaderBoard eg. there could be a method like this
public void Foo()
{
textBlock1.Text = Convert.ToString(this.MyProperty);
}
The class would than look like this:
public partial class LeaderBoard : Page
{
public int MyProperty { get; set; }
//stuff...
public void Foo()
{
textBlock1.Text = Convert.ToString(this.MyProperty);
}
}
If you need to change the MyProperty from a different place while the LeaderBoard is open keep the reference you have created.
To keep the reference change this line
this.NavigationService.Navigate(new LeaderBoard() { MyProperty = 7187, });
To this
_leaderBoard = new LeaderBoard() { MyProperty = 7187, };
this.NavigationService.Navigate(_leaderBoard);
And create a Field in the outer scope like this
private LeaderBoard _leaderBoard; //<- needs to be outside the method
private void Btn_LeaderB_Click(object sender, RoutedEventArgs e)
{
_leaderBoard = new LeaderBoard() { MyProperty = 7187, };
this.NavigationService.Navigate(_leaderBoard);
}
If Btn_LeaderB_Click gets called multiple times but you only want to create 1 LeaderBoard you can do this:
private void Btn_LeaderB_Click(object sender, RoutedEventArgs e)
{
if (_leaderBoard == null)
_leaderBoard = new LeaderBoard() { MyProperty = 7187, };
this.NavigationService.Navigate(_leaderBoard);
}
With that the _leaderBoard field will only get initialized once.
If you want to increase the value MyProperty everytime the Btn_LeaderB_Click method gets called you can further extend it to this:
private void Btn_LeaderB_Click(object sender, RoutedEventArgs e)
{
if (_leaderBoard == null)
_leaderBoard = new LeaderBoard() { MyProperty = 7187, };
else
_leaderBoard.MyProperty += int.Parse(txt_score4_tm1.Text);
this.NavigationService.Navigate(_leaderBoard);
}

C# Reading from multiple text files, splitting lines into a List, and then loading into ListBox

I'm getting a few errors and also my code is unfinished. I was using another Stackoverflow question to set this up to begin with but it wasn't fit to my needs.
I have three text files which the data is split by commas such as "Name,25,25.6" so string, int, decimal. I have all three text files that have three columns like that, same data types, but just different names/numbers.
I have three different list boxes that I want to split them into but I'm having trouble getting the three different split list items to get into three different list boxes. I'll copy and paste all the code I have. I am also using a combo box to allow the user to select the file they want to load into the combo box which I believe I got it right.
The errors I get are in the displayLists(), it says on the lstItemName.DataSource = Inventory; line that Inventory does not exist in the current context. There are also a plenitude of other errors.
Any help will be appreciated, I'll copy and paste my code. I have a Windows Form and I'm using Visual Studio Express 2012 in C#
namespace TCSCapstone
{
public partial class frmInventory : Form
{
public frmInventory()
{
InitializeComponent();
}
string cstrItemName;
int cintNumberOfItems;
decimal cdecPrice;
decimal cdecTotalPrices;
string selectedList = "";
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
selectedList = this.cmbList.GetItemText(this.cmbList.SelectedItem);
if (selectedList == "Creative Construction")//if the selected combo
box item equals the exact string selected
{
selectedList = "creative"; //then the string equals creative,
which is creative.txt but I add the .txt in the btnLoadInfo method
} else if (selectedList == "Paradise Building")
{
selectedList = "paradise";//this is for paradise.txt
}
else if (selectedList == "Sitler Construction")
{
selectedList = "sitler";//this is for sitler.txt
}
else
{
MessageBox.Show("Please select one of the items.");
}
}
private void btnLoadInfo_Click(object sender, EventArgs e)
{
List<frmInventory> Inventory = new List<frmInventory>();
using (StreamReader invReader = new StreamReader(selectedList +
".txt"))
{
while (invReader.Peek() >= 0)
{
string str;
string[] strArray;
str = invReader.ReadLine();
strArray = str.Split(',');
frmInventory currentItem = new frmInventory();
currentItem.cstrItemName = strArray[0];
currentItem.cintNumberOfItems = int.Parse(strArray[1]);
currentItem.cdecPrice = decimal.Parse(strArray[2]);
Inventory.Add(currentItem);
}
}
displayLists();
}//end of btnLoadInfo
void displayLists()
{
int i;
lstItemName.Items.Clear();
lstNumberOfItems.Items.Clear();
lstPrice.Items.Clear();
lstTotalPrices.Items.Clear();
lstItemName.DataSource = Inventory;
lstItemName.ValueMember = "cstrItemName";
lstItemName.DisplayMember = "cintNumberOfItems";
}
}//end of frmInventory
}//end of namespace
I do not know if this is exactly what you need, but try something like this:
public partial class Form2 : Form
{
List<Inventory> inventory;
public Form2()
{
InitializeComponent();
}
public void ReadFiles()
{
if (inventory == null)
inventory = new List<Inventory>();
using (TextReader r = new StreamReader("file.txt"))
{
string line = null;
while ((line = r.ReadLine()) != null)
{
string[] fields = line.Split(',');
Inventory obj = new Inventory();
obj.Name = fields[0];
obj.Qtd = Convert.ToInt32(fields[1]);
obj.Price = Convert.ToInt32(fields[2]);
inventory.Add(obj);
}
}
SetDataSourceList();
}
public void SetDataSourceList()
{
listBox1.DisplayMember = "Name";
listBox2.DisplayMember = "Qtd";
listBox3.DisplayMember = "Price";
listBox1.DataSource =
listBox2.DataSource =
listBox3.DataSource =
inventory;
}
}
public class Inventory
{
public string Name { get; set; }
public int Qtd { get; set; }
public decimal Price { get; set; }
}

Refactoring WPF code behind

I have a method called get Data which executes my SQL and returns some rows of ContactLists containing Aggregated Labels.At the moment this method is in my code behind and would like to move it to a separate Data Access class. I would appreciate your assistance. Thanks!
Is normal, if i understand your code, you do this operation after ContactList initialization:
contactList.Labels = new ObservableCollection<Label>()
{
new Label() {
Name = dr["LABEL_NAME"].ToString(),
Count = Convert.ToInt32(dr["LABEL_COUNT"])
}
};
For each ContactList is always added one item, you will do something like this:
contactList.Labels = new ObservableCollection<Label>();
foreach(var item in <yourLabelDataSource>)
contactList.Labels.Add(new Label(...));
The solution is like this:
Dictionary<int, ContactList> myContactDictionary = new Dictionary<int, ContactList>();
using (DB2DataReader dr = command.ExecuteReader())
{
while (dr.Read())
{
int id = Convert.ToInt32(dr["CONTACT_LIST_ID"]);
if (!myContactDictionary.ContainsKey(id))
{
ContactList contactList = new ContactList();
contactList.ContactListID = id;
contactList.ContactListName = dr["CONTACT_LIST_NAME"].ToString();
contactList.Labels = new ObservableCollection<Label>()
{
new Label()
{
Name = dr["LABEL_NAME"].ToString(),
Count = Convert.ToInt32(dr["LABEL_COUNT"])
}
};
myContactDictionary.Add(id, contactList);
}
else
{
//Add new label because CONTACT_LIST_ID Exists
ContactList contactList = myContactDictionary[id];
contactList.Labels.Add(
new Label()
{
Name = dr["LABEL_NAME"].ToString(),
Count = Convert.ToInt32(dr["LABEL_COUNT"])
}
);
}
}
}
Ben, for your last question you can use this solution:
else
{
//Add new label because CONTACT_LIST_ID Exists
ContactList contactList = myContactDictionary[id];
string name = dr["LABEL_NAME"].ToString();
var label = contactList.Labels.Where(l => l.Name == name).FirstOrDefault();
if( label != null )
label.Count += Convert.ToInt32(dr["LABEL_COUNT"]);
else
{
contactList.Labels.Add(
new Label()
{
Name = dr["LABEL_NAME"].ToString(),
Count = Convert.ToInt32(dr["LABEL_COUNT"])
}
);
}
I hope this code is readable and helpfulL!
}
This is other response:
Create and Object Model that can contain your required data:
public class DataResult
{
public ObservableCollection<AggregatedLabel> AggregatedLabels { get; set; }
public ObservableCollection<ContactList> ContactLists { get; set; }
}
You can build a method that return DataResult object, in your method (GetData()), you can valorize the two different properties (AggregatedLabels and ContactsList) with your DB Result. In the and you can return DataResult Object.
A little example here:
public DataResult GetData()
{
DataResult result = new DataResult();
result.AggregatedLabels = new ObservableCollection<AggregatedLabel>();
result.ContactLists = new ObservableCollection<ContactList>();
// Manipulate data result with your method logic like in this examle:
foreach(var something in dbResult)
{
ContactList cl = new ContactList() {
//Binding from something
}
result.ContactLists.Add(cl);
}
return result; //return your Object Model with required Data!
}
I hope it is conceptually clear

C# grid DataSource polymorphism

I have a grid, and I'm setting the DataSource to a List<IListItem>. What I want is to have the list bind to the underlying type, and disply those properties, rather than the properties defined in IListItem. So:
public interface IListItem
{
string Id;
string Name;
}
public class User : IListItem
{
string Id { get; set; };
string Name { get; set; };
string UserSpecificField { get; set; };
}
public class Location : IListItem
{
string Id { get; set; };
string Name { get; set; };
string LocationSpecificField { get; set; };
}
How do I bind to a grid so that if my List<IListItem> contains users I will see the user-specific field? Edit: Note that any given list I want to bind to the Datagrid will be comprised of a single underlying type.
Data-binding to lists follows the following strategy:
does the data-source implement IListSource? if so, goto 2 with the result of GetList()
does the data-source implement IList? if not, throw an error; list expected
does the data-source implement ITypedList? if so use this for metadata (exit)
does the data-source have a non-object indexer, public Foo this[int index] (for some Foo)? if so, use typeof(Foo) for metadata
is there anything in the list? if so, use the first item (list[0]) for metadata
no metadata available
List<IListItem> falls into "4" above, since it has a typed indexer of type IListItem - and so it will get the metadata via TypeDescriptor.GetProperties(typeof(IListItem)).
So now, you have three options:
write a TypeDescriptionProvider that returns the properties for IListItem - I'm not sure this is feasible since you can't possibly know what the concrete type is given just IListItem
use the correctly typed list (List<User> etc) - simply as a simple way of getting an IList with a non-object indexer
write an ITypedList wrapper (lots of work)
use something like ArrayList (i.e. no public non-object indexer) - very hacky!
My preference is for using the correct type of List<>... here's an AutoCast method that does this for you without having to know the types (with sample usage);
Note that this only works for homogeneous data (i.e. all the objects are the same), and it requires at least one object in the list to infer the type...
// infers the correct list type from the contents
static IList AutoCast(this IList list) {
if (list == null) throw new ArgumentNullException("list");
if (list.Count == 0) throw new InvalidOperationException(
"Cannot AutoCast an empty list");
Type type = list[0].GetType();
IList result = (IList) Activator.CreateInstance(typeof(List<>)
.MakeGenericType(type), list.Count);
foreach (object obj in list) result.Add(obj);
return result;
}
// usage
[STAThread]
static void Main() {
Application.EnableVisualStyles();
List<IListItem> data = new List<IListItem> {
new User { Id = "1", Name = "abc", UserSpecificField = "def"},
new User { Id = "2", Name = "ghi", UserSpecificField = "jkl"},
};
ShowData(data, "Before change - no UserSpecifiedField");
ShowData(data.AutoCast(), "After change - has UserSpecifiedField");
}
static void ShowData(object dataSource, string caption) {
Application.Run(new Form {
Text = caption,
Controls = {
new DataGridView {
Dock = DockStyle.Fill,
DataSource = dataSource,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false
}
}
});
}
As long as you know for sure that the members of the List<IListItem> are all going to be of the same derived type, then here's how to do it, with the "Works on my machine" seal of approval.
First, download BindingListView, which will let you bind generic lists to your DataGridViews.
For this example, I just made a simple form with a DataGridView and randomly either called code to load a list of Users or Locations in Form1_Load().
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using Equin.ApplicationFramework;
namespace DGVTest
{
public interface IListItem
{
string Id { get; }
string Name { get; }
}
public class User : IListItem
{
public string UserSpecificField { get; set; }
public string Id { get; set; }
public string Name { get; set; }
}
public class Location : IListItem
{
public string LocationSpecificField { get; set; }
public string Id { get; set; }
public string Name { get; set; }
}
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void InitColumns(bool useUsers)
{
if (dataGridView1.ColumnCount > 0)
{
return;
}
DataGridViewCellStyle gridViewCellStyle = new DataGridViewCellStyle();
DataGridViewTextBoxColumn IDColumn = new DataGridViewTextBoxColumn();
DataGridViewTextBoxColumn NameColumn = new DataGridViewTextBoxColumn();
DataGridViewTextBoxColumn DerivedSpecificColumn = new DataGridViewTextBoxColumn();
IDColumn.DataPropertyName = "ID";
IDColumn.HeaderText = "ID";
IDColumn.Name = "IDColumn";
NameColumn.DataPropertyName = "Name";
NameColumn.HeaderText = "Name";
NameColumn.Name = "NameColumn";
DerivedSpecificColumn.DataPropertyName = useUsers ? "UserSpecificField" : "LocationSpecificField";
DerivedSpecificColumn.HeaderText = "Derived Specific";
DerivedSpecificColumn.Name = "DerivedSpecificColumn";
dataGridView1.Columns.AddRange(
new DataGridViewColumn[]
{
IDColumn,
NameColumn,
DerivedSpecificColumn
});
gridViewCellStyle.SelectionBackColor = Color.LightGray;
gridViewCellStyle.SelectionForeColor = Color.Black;
dataGridView1.RowsDefaultCellStyle = gridViewCellStyle;
}
public static void BindGenericList<T>(DataGridView gridView, List<T> list)
{
gridView.DataSource = new BindingListView<T>(list);
}
private void Form1_Load(object sender, EventArgs e)
{
dataGridView1.AutoGenerateColumns = false;
Random rand = new Random();
bool useUsers = rand.Next(0, 2) == 0;
InitColumns(useUsers);
if(useUsers)
{
TestUsers();
}
else
{
TestLocations();
}
}
private void TestUsers()
{
List<IListItem> items =
new List<IListItem>
{
new User {Id = "1", Name = "User1", UserSpecificField = "Test User 1"},
new User {Id = "2", Name = "User2", UserSpecificField = "Test User 2"},
new User {Id = "3", Name = "User3", UserSpecificField = "Test User 3"},
new User {Id = "4", Name = "User4", UserSpecificField = "Test User 4"}
};
BindGenericList(dataGridView1, items.ConvertAll(item => (User)item));
}
private void TestLocations()
{
List<IListItem> items =
new List<IListItem>
{
new Location {Id = "1", Name = "Location1", LocationSpecificField = "Test Location 1"},
new Location {Id = "2", Name = "Location2", LocationSpecificField = "Test Location 2"},
new Location {Id = "3", Name = "Location3", LocationSpecificField = "Test Location 3"},
new Location {Id = "4", Name = "Location4", LocationSpecificField = "Test Location 4"}
};
BindGenericList(dataGridView1, items.ConvertAll(item => (Location)item));
}
}
}
The important lines of code are these:
DerivedSpecificColumn.DataPropertyName = useUsers ? "UserSpecificField" : "LocationSpecificField"; // obviously need to bind to the derived field
public static void BindGenericList<T>(DataGridView gridView, List<T> list)
{
gridView.DataSource = new BindingListView<T>(list);
}
dataGridView1.AutoGenerateColumns = false; // Be specific about which columns to show
and the most important are these:
BindGenericList(dataGridView1, items.ConvertAll(item => (User)item));
BindGenericList(dataGridView1, items.ConvertAll(item => (Location)item));
If all items in the list are known to be of the certain derived type, just call ConvertAll to cast them to that type.
You'll need to use a Grid template column for this. Inside the template field you'll need to check what the type of the object is and then get the correct property - I recommend creating a method in your code-behind which takes care of this. Thus:
<asp:TemplateField HeaderText="PolymorphicField">
<ItemTemplate>
<%#GetUserSpecificProperty(Container.DataItem)%>
</ItemTemplate>
</asp:TemplateField>
In your code-behind:
protected string GetUserSpecificProperty(IListItem obj) {
if (obj is User) {
return ((User) obj).UserSpecificField
} else if (obj is Location) {
return ((Location obj).LocationSpecificField;
} else {
return "";
}
}
I tried projections, and I tried using Convert.ChangeType to get a list of the underlying type, but the DataGrid wouldn't display the fields. I finally settled on creating static methods in each type to return the headers, instance methods to return the display fields (as a list of string) and put them together into a DataTable, and then bind to that. Reasonably clean, and it maintains the separation I wanted between the data types and the display.
Here's the code I use to create the table:
DataTable GetConflictTable()
{
Type type = _conflictEnumerator.Current[0].GetType();
List<string> headers = null;
foreach (var mi in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
{
if (mi.Name == "GetHeaders")
{
headers = mi.Invoke(null, null) as List<string>;
break;
}
}
var table = new DataTable();
if (headers != null)
{
foreach (var h in headers)
{
table.Columns.Add(h);
}
foreach (var c in _conflictEnumerator.Current)
{
table.Rows.Add(c.GetFieldsForDisplay());
}
}
return table;
}
When you use autogeneratecolumns it doesnt automatically do this for you?
My suggestion would be to dynamically create the columns in the grid for the extra properties and create either a function in IListItem that gives a list of available columns - or use object inspection to identify the columns available for the type.
The GUI would then be much more generic, and you would not have as much UI control over the extra columns - but they would be dynamic.
Non-checked/compiled 'psuedo code';
public interface IListItem
{
IList<string> ExtraProperties;
... your old code.
}
public class User : IListItem
{
.. your old code
public IList<string> ExtraProperties { return new List { "UserSpecificField" } }
}
and in form loading
foreach(string columnName in firstListItem.ExtraProperties)
{
dataGridView.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = columnName, HeaderText = columnName );
}
If you are willing to use a ListView based solution, the data-bindable version ObjectListView will let you do this. It reads the exposed properties of the DataSource and creates columns to show each property. You can combine it with BindingListView.
It also looks nicer than a grid :)

Categories