WPF DataGrid : How to add a row thats content is defined by an array of Object? - wpf

I have created several Datagrids dynamically. In the mode of dynamic Datagrid creation the number and name of columns are not known in advance.
So, I created a DataGrid like below :
DataGrid grid = new DataGrid();
grid.Columns.Add(new DataGridTextColumn() { Header = randomHeader1 });
grid.Columns.Add(new DataGridTextColumn() { Header = randomHeader2 });
grid.Columns.Add(new DataGridTextColumn() { Header = randomHeader3 });
// The number of columns for each datagrid is variable
How can I add rows to my Datagrid? For instance, I want to add those 3 rows to Dtatagrid that I have defined in the top:
a, b, c
d, e, f
g, h, i
I tried to solve my problem like below but it is not working:
grid.Items.Add(new Object[] { "a", "b", "c" });
grid.Items.Add(new Object[] { "d", "e", "f" });
grid.Items.Add(new Object[] { "g", "h", "i" });
Please review the code and provide me solution.

Using reflection, you could create a collection of dynamic objects, then bind your DataGrid to it.
There is a great answer here about creating dynamic classes:
How to dynamically create a class in C#?
Using this, you can create the list of properties (which will be columns in your datagrid) at runtime like this
List<TypeBuilderNamespace.Field> myFields = new List<TypeBuilderNamespace.Field>();
myFields.Add( new TypeBuilderNamespace.Field("FirstName", Type.GetType("System.String")));
myFields.Add( new TypeBuilderNamespace.Field("Surname", Type.GetType("System.String")));
myFields.Add( new TypeBuilderNamespace.Field("Age", Type.GetType("System.Int32")));
Then dynamically create your class
Type myDynamicType = TypeBuilderNamespace.MyTypeBuilder.CompileResultType(myFields);
Create some sample data
List<dynamic> people = new List<dynamic>();
dynamic person1 = Activator.CreateInstance(myDynamicType);
person1.FirstName = "John";
person1.Surname = "Smith";
person1.Age = 45;
people.Add(person1);
dynamic person2 = Activator.CreateInstance(myDynamicType);
person2.FirstName = "Emma";
person2.Surname = "Jones";
person2.Age = 18;
people.Add(person2);
Then bind your data to the grid
DataGrid grid = new DataGrid();
grid.AutoGenerateColumns = true;
grid.ItemsSource = people;
This gives the following DataGrid
I modified the code in the answer quoted above slightly, so here is my listing of that for completeness:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace TypeBuilderNamespace {
public class Field {
public String FieldName { get; set; }
public Type FieldType { get; set; }
public Field(String name, Type type) {
FieldName = name;
FieldType = type;
}
}
public static class MyTypeBuilder {
public static Type CompileResultType(List<Field> fields) {
TypeBuilder tb = GetTypeBuilder();
ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
FieldName(string) and FieldType(Type)
foreach (var field in fields)
CreateProperty(tb, field.FieldName, field.FieldType);
Type objectType = tb.CreateType();
return objectType;
}
private static TypeBuilder GetTypeBuilder() {
var typeSignature = "MyDynamicType";
var an = new AssemblyName(typeSignature);
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder tb = moduleBuilder.DefineType(typeSignature
, TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout
, null);
return tb;
}
private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType) {
FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
ILGenerator getIl = getPropMthdBldr.GetILGenerator();
getIl.Emit(OpCodes.Ldarg_0);
getIl.Emit(OpCodes.Ldfld, fieldBuilder);
getIl.Emit(OpCodes.Ret);
MethodBuilder setPropMthdBldr =
tb.DefineMethod("set_" + propertyName,
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.HideBySig,
null, new[] { propertyType });
ILGenerator setIl = setPropMthdBldr.GetILGenerator();
Label modifyProperty = setIl.DefineLabel();
Label exitSet = setIl.DefineLabel();
setIl.MarkLabel(modifyProperty);
setIl.Emit(OpCodes.Ldarg_0);
setIl.Emit(OpCodes.Ldarg_1);
setIl.Emit(OpCodes.Stfld, fieldBuilder);
setIl.Emit(OpCodes.Nop);
setIl.MarkLabel(exitSet);
setIl.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getPropMthdBldr);
propertyBuilder.SetSetMethod(setPropMthdBldr);
}
}
}

Related

Windows Forms Custom DataGridView

I am not very experienced with Windows Forms and am not pretty sure how I should tackle with this task the best way possible. I have a class which looks like this:
public class VariableMapping
{
private string variableName;
private string variableText;
private string variableSelector;
public VariableMapping(string variableName, string variableText, string variableSelector)
{
this.VariableName = variableName;
this.VariableText = variableText;
this.VariableSelector = variableSelector;
}
public string VariableName
{
get { return this.variableName; }
set { this.variableName = value; }
}
public string VariableText
{
get { return this.variableText; }
set { this.variableText = value; }
}
public string VariableSelector
{
get { return this.variableSelector; }
set { this.variableSelector = value; }
}
}
I want to create a DataGridView which should be bound to a number of elements of type VariableMapping in a list. However, I want only 1 of the properties(VariableText) of every instance to be shown in the DataGridView but I want to be able to address the whole object through the DataGrid when I need to. I also need to add 2 more custom columns: a ComboBox with predefined values and a NumberBox.
It might seem a really simple task but I'm trully unexperienced in WinForms and couldn't find a solution I can use already. Thank you!
Edit: I am trying something like this but it doesn't seem to work properly:
public partial class MappingTable : Form
{
private DataGridView dataGridView1 = new DataGridView();
public MappingTable(List<VariableMapping> variableMappings)
{
InitializeComponent();
var colors = new List<string>() { "#color_k1", "#color_k2", "#color_s1" };
dataGridView1.AutoGenerateColumns = false;
dataGridView1.AutoSize = true;
dataGridView1.DataSource = variableMappings;
DataGridViewColumn titleColumn = new DataGridViewColumn();
titleColumn.DataPropertyName = "VariableText";
titleColumn.HeaderText = "Variable";
titleColumn.Name = "Variable*";
dataGridView1.Columns.Add(titleColumn);
DataGridViewComboBoxColumn colorsColumn = new DataGridViewComboBoxColumn();
colorsColumn.DataSource = colors;
colorsColumn.HeaderText = "Color";
dataGridView1.Columns.Add(colorsColumn);
DataGridViewTextBoxColumn opacityColumn = new DataGridViewTextBoxColumn();
opacityColumn.HeaderText = "Opacity";
dataGridView1.Columns.Add(opacityColumn);
this.Controls.Add(dataGridView1);
this.AutoSize = true;
}
}

Specified element is already a logical child of another element

I hope this doesn't get marked as duplicate since my problem is kinda complex, so none of the other answers helped. I have a class called 'ControlChoiceModule' that generates a System.Windows.Controls object based on the type of property it deals with (String - TextBox, Boolean - CheckBox, DateTime - DatePicker, etc..).
It has two dictionaries:
public static class ControlChoiceModule
{
private static readonly Dictionary<Type, object> TypeToControl = new Dictionary<Type, object>
{
{typeof(bool), new CheckBox() },
{typeof(DateTime), new DatePicker() }
};
private static readonly Dictionary<Type, DependencyProperty> ControlToProperty = new Dictionary<Type, DependencyProperty>
{
{typeof(TextBox), TextBox.TextProperty },
{typeof(CheckBox), CheckBox.IsCheckedProperty },
{typeof(DatePicker), DatePicker.SelectedDateProperty }
};
The purpose of the other one is just for binding. And here are the two methods:
public static object GenerateControl(Type theType, Binding B)
{
object O;
if (TypeToControl.ContainsKey(theType))
{
O = TypeToControl[theType];
}
else
{
O = new TextBox();
}
SetBinding(O, B);
return O;
}
private static void SetBinding(object O, Binding B)
{
BindingOperations.SetBinding(O as DependencyObject, ControlToProperty[O.GetType()], B);
}
Now the purpose of all this is to generate an insertion window for a certain class, generically. So the windows loops through all of the class's properties and, based on the type, generates an appropriate field for it.
private void GenerateInsertionOrUpdatePage(string windowText)
{
var w2 = new InsertionWindow();
w2.DataContext = this.DataContext;
w2.Title = windowText;
w2.Show();
foreach (var P in ReturnPropertyList())
{
if (P.Name != "SearchableString" && P.Name != "Id" )
{
Label L = new Label();
L.Content = P.Name + ":";
w2.InsertionStackPanel.Children.Add(L);
Binding B = new Binding();
B.Path = new PropertyPath("NewT." + P.Name);
B.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
B.Mode = BindingMode.TwoWay;
**w2.InsertionStackPanel.Children.Add(ControlChoiceModule.GenerateControl(P.PropertyType, B) as UIElement);**
}
}
}
The method above gets called on a click of a button. The first time I click it, it works just fine. But when I click it again, I get the error from the title (on the line marked with **).
Any idea why this happens?
Thanks

How to eradicate duplicate items while adding in Existing WPF ListView

In my WPF window, I have a list view (lvw1) of 3 columns. While adding a new list view item, I want to check if item doesn't exist already. I'm using the following line of code
if (!lvw1.Items.Containskey(keyitem))
Keyitem is the string cross-checked with existing items. I guess Containskey method doesn't work in wpf.
Please suggest alternative/appropriate code.
You can use the Contains() method, just assign the new object you would create to a variable and check if it exists already in the ListView:
var newObject = new { Col1 = txt1.Text, Col2 = txt2.Text, Col3 = txt3.Text };
if(!lvw1.Items.Contains(newObject)){
lvw1.Items.Add(newObject);
}
Update to make it ignore the case:
Probably the easiest way to do that is to create a class for your type and override Equals.
This works because ItemCollection's (the type of Items) Contains method uses the Equals method internally to check equality of the containing objects.
public class Item
{
public string Text1 { get; set; }
public string Text2 { get; set; }
public string Text3 { get; set; }
public Item(string text1, string text2, string text3)
{
this.Text1 = text1;
this.Text2 = text2;
this.Text3 = text3;
}
public override bool Equals(object obj)
{
var compareObject = obj as Item;
if (compareObject == null) return false;
return Text1.ToLower().Equals(compareObject.Text1.ToLower()) &&
Text2.ToLower().Equals(compareObject.Text2.ToLower()) &&
Text3.ToLower().Equals(compareObject.Text3.ToLower());
}
}
Then, instead of creating an anonymous object, create an instance of this type.
var newObject = new Item(txt1.Text, txt2.Text, txt3.Text); //use new Item() here
if(!lvw1.Items.Contains(newObject)){
lvw1.Items.Add(newObject);
}
You can find the item by text:
ListViewItem item = lvw1.FindItemWithText("item text");
if (item == null)
{
// does not exist. add to list
}
You could use the ItemCollection.IndexOf Method like this:
if (lvw1.Items.IndexOf(keyitem) < 0)
{
// add the object
}
Note this will implicitely use the keyitem object's Equals method, whatever that does. If it's not ok, then you'll have to enumerate the whole collection (using Linq methods for example) for something that suits your needs.
Try this
using System.Linq;
...
object o = new { Col1 = txt1.Text, Col2 = txt2.Text, Col3 = txt3.Text });
if (!lvw1.Items.Cast<dynamic>().Any(d => IsEqual(d, o))) {
// add item
}
...
private static bool IsEqual(dynamic a, dynamic b) {
return a.Col1 == b.Col1 && a.Col2 == b.Col2 && a.Col3 == b.Col3;
}

Basically I have a data grid view in windows form. And I have added a combo box as a column

This is my object structure
class object
{
string projectname;
string projectid;
list<string> associated_students;
}
//The List I am binding to the grid
list<objects> objectList = getList();
dataGridView.Source =objectList;
Now I want to bind the combo box inside the datagrid with the list "associated_students"
If I understand the question, you want each row to be tied to an object within your list of objects and you want the third column to show a combobox of that object's unique list of associated students. If I am correct, a simple search leads to this similar question:
How do I set up a DataGridView ComboBoxColumn with a different DataSource in each cell?
To solve, you need to manually bind each row. I was able to duplicate your problem and came up with this solution:
Your class "object"
public class Assignment
{
public Assignment()
{
this.Associated_Students = new List<string>();
}
public string ProjectName { get; set; }
public string ProjectID { get; set; }
public List<string> Associated_Students { get; set; }
}
And in Form1:
public Form1()
{
InitializeComponent();
this.Assignments = new List<Assignment>()
{
new Assignment()
{
ProjectID = "1",
ProjectName = "First",
Associated_Students = new List<string>() { "Me", "You", "Him", "Her" }
},
new Assignment()
{
ProjectID = "2",
ProjectName = "Second",
Associated_Students = new List<string>() { "Foo", "Bar" }
}
};
this.BindDGViewToList();
}
public List<Assignment> Assignments { get; set; }
public void BindDGViewToList()
{
DataGridViewTextBoxColumn col1 = new DataGridViewTextBoxColumn();
col1.Name = "Project Name";
col1.ValueType = typeof(string);
dataGridView1.Columns.Add(col1);
DataGridViewTextBoxColumn col2 = new DataGridViewTextBoxColumn();
col2.Name = "Project ID";
col2.ValueType = typeof(string);
dataGridView1.Columns.Add(col2);
DataGridViewComboBoxColumn col3 = new DataGridViewComboBoxColumn();
col3.Name = "Associated Students";
col3.ValueType = typeof(string);
dataGridView1.Columns.Add(col3);
for (int i = 0; i < this.Assignments.Count; i++)
{
DataGridViewRow row = (DataGridViewRow)(dataGridView1.Rows[0].Clone());
DataGridViewTextBoxCell textCell = (DataGridViewTextBoxCell)(row.Cells[0]);
textCell.ValueType = typeof(string);
textCell.Value = this.Assignments[i].ProjectName;
textCell = (DataGridViewTextBoxCell)(row.Cells[1]);
textCell.ValueType = typeof(string);
textCell.Value = this.Assignments[i].ProjectID;
DataGridViewComboBoxCell comboCell = (DataGridViewComboBoxCell)(row.Cells[2]);
comboCell.ValueType = typeof(string);
comboCell.DataSource = this.Assignments[i].Associated_Students;
dataGridView1.Rows.Add(row);
}
}
Note: This will display what you are asking for but you will have to handle updating your data. I would suggest researching BindingList over List objects. There may be better solutions, but this worked quickly for me.

DataGrid 'EditItem' is not allowed for this view when dragging multiple items

I have a datagrid which gets data like this:
public struct MyData
{
public string name { set; get; }
public string artist { set; get; }
public string location { set; get; }
}
DataGridTextColumn col1 = new DataGridTextColumn();
col4.Binding = new Binding("name");
dataGrid1.Columns.Add(col1);
dataGrid1.Items.Add((new MyData() { name = "Song1", artist = "MyName", location = "loc"}));
dataGrid1.Items.Add((new MyData() { name = "Song2", artist = "MyName", location = "loc2"}));
The problem is- whenever a user tries to edit a cell or drags multiple cells- the app throws an exception:
System.InvalidOperationException was unhandled
Message: 'EditItem' is not allowed for this view.
Why is this? Is it because of the way the data is entered?
Any ideas?
Thanks!
I got this issue when assigning ItemsSource to IEnumerable<T>.
I fixed it by converting the IEnumberable<T> to a List<T> and then assigning that to ItemsSource.
I'm not sure why using IEnumerable caused that issue, but this change fixed it for me.
Instead of using a struct use a class instead.
UPDATED ANSWER: Try adding your MyData instances to a List then assigning that list to the DataGrid.ItemsSource
If you use datagrid DataGridCheckBoxColumn you need to set <Setter Property="IsEditing" Value="true" />
on check box column. See this: https://stackoverflow.com/a/12244451/1643201
This answer is not my own, just the working code example suggested by AnthonyWJones.
public class MyData //Use class instead of struct
{
public string name { set; get; }
public string artist { set; get; }
public string location { set; get; }
}
DataGridTextColumn col1 = new DataGridTextColumn();
col4.Binding = new Binding("name");
dataGrid1.Columns.Add(col1);
dataGrid1.Items.Add((new MyData() { name = "Song1", artist = "MyName", location = "loc"}));
dataGrid1.Items.Add((new MyData() { name = "Song2", artist = "MyName", location = "loc2"}));
//Create a list of MyData instances
List<MyData> myDataItems = new List<MyData>();
myDataItems.Add(new MyData() { name = "Song1", artist = "MyName", location = "loc"});
myDataItems.Add(new MyData() { name = "Song2", artist = "MyName", location = "loc2"});
//Assign the list to the datagrid's ItemsSource
dataGrid1.ItemsSource = items;
For my case,
processLimits.OrderBy(c => c.Parameter);
returns an
IOrderedEnumerable<ProcessLimits>
not a
List<ProcessLimits>
so when I assign a style for my event setter to a checkbox column in my datagrid
style.Setters.Add(new EventSetter(System.Windows.Controls.Primitives.ToggleButton.CheckedEvent, new RoutedEventHandler(ServiceActiveChecked)));
ServiceActiveChecked is never called and I got
'EditItem' is not allowed for this view.
and for anyone else doing checkboxes in datagrid columns, I use a column object with my column data in this constructor for adding the data grid I use with adding the style above.
datagridName.Columns.Add(new DataGridCheckBoxColumn()
{
Header = column.HeaderText.Trim(),
Binding = new System.Windows.Data.Binding(column.BindingDataName.Trim()) { StringFormat = column.StringFormat != null ? column.StringFormat.Trim().ToString() : "" },
IsReadOnly = column.IsReadOnlyColumn,
Width = new DataGridLength(column.DataGridWidth, DataGridLengthUnitType.Star),
CellStyle = style,
});
I solved this by setting the datagrid's source after the InitializeComponent:
public MainWindow()
{
InitializeComponent();
FilterGrid.ItemsSource = ScrapeFilter;
}

Resources