I'm autogenerating columns based on a set. After that, I'm adding one of my own, manually, like this.
private void OnAutoGeneratedColumns(Object sender, EventArgs eventArgs)
{
DataGridColumn column = new DataGridTextColumn { ... };
DataGrid grid = sender.Get<DataGrid>();
grid.Columns.Add(column);
}
It adds the column and I can see the header being set according to the code. However, I'm not sure how to add any data to it. Checking the list of properties didn't offer me a clue. How should one do that?
The additional column is to be rendered based on the information from the same line in the grid (e.g. if the autogenerated columns are FirstName and LastName, the manually added one would be FullName and consist of the concatenation of them).
One approach I can think of is to inherit the class that is being used to autogenerate the columns, extend it with a new property and put it in the constructor of my view model like so. Then I could bind to the latter instead of the former.
BasicView = new ListCollectionView(basicThings);
ExtendedView = new ListCollectionView(extendedThings);
class ExtendedThing : BasicThing
{
public String FullName { get { return FirstName + LastName; } }
}
But that seems like a rather major intrusion into the code, which makes me suspect I'm heading in the wrong direction...
Related
I have a DataTable (DataTable) made of 3 columns: Time(double), Values(double), Digitals(DigitalModel)
DataTable is displayed in a DataGrid where columns are generated automatically and the last one is being intercepted during creation and a specific data template been applied to correctly display the DigitalModel type.
I'm using Caliburn Micro and Mahapps libraries for UI
The Digital Model class is defined as:
public class DigitalModel : PropertyChangedBase{
public DigitalModel(){
SourceDigital = new BindableCollection<string>();
SelectedDigital = new BindableCollection<string>();
}
public BindableCollection<string> SourceDigital{ get; set; }
public BindableCollection<string> SelectedDigital{ get; set; }
}
I need to be able to sort the DataTable through the time column. So I use DataTable.DefaultView.Sort = "Time"
For testing purpose, only one row is sorted at a time, using the CellEditEndingevent and sorting when a "Time" cell has been updated.
(I'm going incrementally here instead of using the sorting functionality of the Mahapps DataGrid Control.
It does work correctly for the Time value and Values value. They are being correctly recopied to the new table position but the last column some of the data is being lost...
So here is the behavior when I sort it (from a view perspective)
Before:
SourceDigital = {"one", "two", "three'}
SelectedDigital = {"two"}
After sorting I have:
SourceDigital = {"one", "two", "three'}
SelectedDigital = {}
I debugged through the CellEditEnding event and also through the CurrentCellChanged, to see the before and after, and it seems like the sort method keeps all the data correctly. In between CellEditEnding and CurrentCellChanged something is messing my third column and clearing one of the collection.
It seems to be a UI issue ? But through the debugging in the ViewModel, I can see that the DataRowView being passed by the CurrentCellChanged event, doesn't the correct object in the third column....
So confused....
Thank you for your help
I have a use case where I am showing data from database in DataGridView, but when I add new rows on the UI (there is no code for adding new row, when I click the button I get all rows from DataGridView - old ones from database and new ones added on the UI), I need to know what rows are added from the UI. Is there a way to know it without checking in database and comparing it? I would like to know it from the UI. I have tried with ISNewRow() but it is true only for the last row.
When using a DataGridView people tend to fiddle with rows and cells directly. Although this is possible, it is way more easy to use data binding via property DataSource.
Using the visual studio designer, you have added a DataGridView and the columns that you want to show. Although it is possible to define which column should show which property, it is easier to do this in the constructor.
Alas you forgot to tell us what items you want to show, so let's assume you want to show several properties of a Product:
class Product
{
public int Id {get; set;}
public string Description {get; set;}
public decimal Price {get; set;}
public int Stock {get; set;}
...
}
Suppose you want to show these values as columns in your DataGridView. In the constructor you'll write:
public MyForm()
{
InitializeComponent(); // Creates the DataGridView and the columns
// define which column should show which property
this.ColumnId.DataPropertyName = nameof(Product.Id);
this.ColumnDescription.DataPropertyName = nameof(Product.Description);
this.ColumnPrice.DataPropertyName = nameof(Product.Price);
this.ColumnStock.DataPropertyName = nameof(Product.Stock);
... // etc, for all Product properties that you want to show
}
Somewhere, you'll have a method to fetch the products from the database:
private IEnumerable<Product> FetchProductsToDisplay()
{
... // TODO: implement; out of scope of this question
}
To display Products: assign them via a BindingList<Product> to the DataSource of the DataGridView:
private BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.DataGridView1.DataSource;
set => this.DataGridView1.DataSource = value;
}
At loading form, or after some other event, you need to fill the datagridview with the Products:
private void InitializeTableProducts()
{
IEnumerable<Product> productsToDisplay = this.FetchProductsToDisplay();
this.DisplayedProducts = new BindingList<Product>(productsToDisplay.ToList());
}
This is enough to display the Products. The operator can add / remove Products, and edit Cells, according to the editability you defined in the columns and the datagridview. If allowed he can even rearrange the rows and columns.
When the operator has finished editing, he notifies the software for instance by pressing the OK button:
private void OnButtonOk_Clicked(object sender, ...)
{
this.ProcessEditedProducts();
}
private void ProcessEditedProducts()
{
ICollection<Product> editedProducts = this.DisplayedProducts;
this.ProcessProducts(editedProducts);
}
In ProcessProducts, you'll have to find out which Products are added / removed / changed. I assume you'll recognize an edited product by a zero Id.
To find out if a Product is removed: it will have an Id in the original database, but not in the editedProducts.
Changed Products are more difficult: you need to compare by value. For this you need to create an IEqualityComparer<Product>.
I'll assume you know the techniques how to check if the Product is added / removed / changed.
I am trying to set a value for DataGridViewRow.Tag when data binding but I don't know how to do it?
I tried with DataRow row = table.NewRow(); But row doesn't have tag.
How to set a value for DataGridViewRow.Tag when binding DataGridView to table (for example)? Or it isn't possible?
Edit1:
Here is the code i am using:
var table = new DataTable();
table.Columns.Add("Title", typeof(string));
table.Columns.Add("URL", typeof(string));
table.Columns.Add("Read Later", typeof(bool));
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
var id = node.Attribute("id").Value;
var url = node.Element("path").Value;
var comment = node.Element("title").Value;
var readlater = node.Attribute("readLater")?.Value.ToString() == "1";
var row = table.NewRow();
row.ItemArray = new object[] { url, comment, readlater };
table.Rows.Add(row);//Edit2
}
dataGridView1.DataSource = table;
I am trying to set a tag for the row to use it in CellClick event:
var cRow = dataGridView1.Rows[e.RowIndex];
var id = cRow.Tag.ToString();
Separate the data from how it is displayed
When using a DataGridView, it is seldom a good idea to access the cells and the columns directly. It is way more easier to use DataGridView.DataSource.
In modern programming there is a tendency to separate your data (= model) from the way your data is displayed (= view). To glue these two items together an adapter class is needed, which is usually called the viewmodel. Abbreviated these three items are called MVVM.
Use DataGridView.DataSource to display the data
Apparently, if the operator clicks a cell, you want to read the value of the tag of the row of the cell, to get some extra information, some Id.
This displayed row is the display of some data. Apparently part of the functionality of this data is access to this Id. You should not put this information in the view, you should put it in the model.
class MyWebPage // TODO: invent proper identifier
{
public int Id {get; set;}
public string Title {get; set;}
public string Url {get; set;}
public bool ReadLater {get; set;}
... // other properties
}
Apparently you have a method to fetch the data that you want to display from a sequence of nodes. Separate fetching this data (=model) from displaying it (= view):
IEnumerable<MyWebPage> FetchWebPages(...)
{
...
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
bool readLater = this.CreateReadLater(node);
yield return new MyWebPage
{
Id = node.Attribute("id").Value,
Url = node.Element("path").Value,
Title = node.Element("title").Value,
ReadLater = this.CreateReadLater(node),
};
}
}
I don't know what is in node "ReadLater", apparently you know how to convert it to a Boolean.
bool CreateReadLater(XElement node)
{
// TODO: implement, if null return true; if not null ...
// out of scope of this question
}
For every property that you want to display you create a DataGridViewColumn. Property DataPropertyName defines which property should be shown in the column. Use DefaultCellStyle if a standard ToString is not enough to display the value properly, for instance, to define the number of digits after the decimal point, or to color negative values red.
You can do this using the visual studio designer, or you can do this in the constructor:
public MyForm
{
InitializeComponents();
this.dataGridViewColumnTitle.DataPropertyName = nameof(MyWebPage.Title);
this.dataGridViewColumnUrl.DataPropertyName = nameof(MyWebPage.Url);
...
}
You don't want to display the Id, so there is no column for this.
Now to display the data, all you have to do is assign the list to the datasource:
this.dataGrieViewWebPages.DataSource = this.FetchWebPages().ToList();
This is display only. If the operator can change the displayed values, and you want to access the changed values, you should put the items in an object that implements interface IBindingList, for instance, using class (surprise!) BindingList<T>:
private BindingList<MyWebPage> DisplayedWebPages
{
get => (BindingList<MyWebPage>)this.dataGrieViewWebPages.DataSource;
set => this.dataGrieViewWebPages.DataSource = value;
}
Initialization:
private void DisplayWebPages()
{
this.DisplayedWebPages = new BindingList<MyWebPage>(this.FetchWebPages.ToList());
}
And presto! All webpages are displayed. Every change that the operator makes: add / remove / edit rows are automatically updated in the DisplayedWebPages.
If you want to access the currently selected WebPages:
private MyWebPage CurrentWebPage =>(MyWebPage)this.dataGrieViewWebPages.CurrentRow?.DataBoundItem;
private IEnumerable<MyWebPage> SelectedWebPages =>
this.dataGrieViewWebPages.SelectedRows
.Cast<DataGridViewRow>()
.Select(row => row.DataBoundItem)
.Cast<MyWebPage>();
Now apparently whenever the operator clicks a cell, you want to do something with the Id of the WebPage that is displayed in the Row of the cell.
View: Displayed Cell and Row
ViewModel: React when operator clicks a cell
Model Action that must be done
React on Cell Click: get the Id
We've handled the View above. ViewModel is the event handler:
void OnDatGridViewCellClicked(object sender, DataGridViewCellEventArgs e)
{
// use the eventArgs to fetch the row, and thus the WebPage:
MyWebPage webPage = (MyWebPage)this.dataGridViewWebPages.Rows[e.RowIndow].DataBoundItem;
this.ProcessWebPage(webPage);
}
ProcessWebPage is typically a method in your Model class:
public void ProcessWebPage(MyWebPage webPage)
{
// Do what you need to do if the operator clicks the cell, for example:
int webPageId = webPage.Id;
...
}
Conclusion: advantages of separating model from view
By the way, did you see that all ViewModel methods are one-liners? Only your Model methods FetchWebPages and ProcessWebPage contain several lines.
Because you separated the View from the Model, changes to your Model or your View will be fairly simple:
If you want to store your data in Json format, or in a database instead of in an XML, your View won't change
If you don't want to react on cell click, but on Button OK click, then your Model won't change. Your model also doesn't have to change if you decide to show more or less columns
Because you separated your Model from your View, the Model can be unit tested without a form. You can also test the View with a Model filled with only test values.
I am programming UI with WPF for a month. So i am new in this world. This is my first ever post here and my motherlanguage isn't English. So if there is any mistake in my writing, sorry for that.
I have a problem with WPF and I am not able to find a solution for it. I am trying to make completely customizable DataGrid in WPF and I don't want the DataGrid to create columns or rows automatically.
I have an ObservableCollection named Sinif_Tutucu (means Class_Holder in my motherlanguage) which holds classes. These classes have the same Field and Properties, but their values are different. I am creating the columns one by one according to Fields and Properties in these classes. After this I am adding columns to DataGrid manually.
Then I am adding the rows with ItemsSource like this.
DataGrid_1.ItemsSource = Sinif_Tutucu;
This populates only properties in the DataGrid and columns made up of fields remain empty. I have searched this and i guess there is no way to bind Fields to DataGrid. Classes within Sinif_Tutucu have thousands of fields. So I can't turn fields to properties and I don't want to code the whole datagrid structure from scratch. Because WPF DataGrid structure offers many useful features which i am currently using. I am able to get Field names and their values or Property names and values. I just need to create each row myself (like columns) and populate their cells one by one with my hand with C#. Or somehow I need to convert these Fields to Properties programmatically. Is there any way to achieve this?
Supposing your class that you want to show in the datagrid looks like this
public class Data
{
public int Value;
public string Name;
public DateTime Date = DateTime.Now;
}
Because you cannot bind fields to DataGridColumns, you have to dynamically create the columns and the records. Here is a short snippet which you can use to do that. This method creates new dynamic objects in a List, which stores the same data as in your ObservableCollection. This List is set as the new ItemSource of your DataGrid. This snippet only creates DataGridTextColumns. You migth have to modify this solution, if want to use other DataGridColumns.
private void loadDataGrid()
{
List<dynamic> dataObjects = new List<dynamic>();
dynamic dataObject;
IDictionary<string, object> dataObjectValues;
var fields = typeof(Data).GetFields();
foreach (var item in Sinif_Tutucu)
{
dataObject = new System.Dynamic.ExpandoObject();
foreach (var field in fields)
{
dataObjectValues = (IDictionary<string, object>)dataObject;
dataObjectValues[field.Name] = field.GetValue(item);
}
dataObjects.Add(dataObject);
}
dataObjectValues = (IDictionary<string, object>)dataObjects[0];
foreach (var pair in dataObjectValues)
{
var binding = new Binding(pair.Key);
DataGridTextColumn col = new DataGridTextColumn() { Binding = binding, Header = pair.Key }; //You might want to use another DataGridColumn-Type
dataGrid.Columns.Add(col);
}
dataGrid.ItemsSource = dataObjects;
}
I have such a situaton, that I am reading txt file making some operation on the lines and at the end I want to display everything in gridview. I have 3 separated columns. In first and second one I am displaying normal string values. But in middle one I have object returned by one class and I would like to display it normally in my gridview. How can I achieve it? I have something like this so far.
while ((line = sr.ReadLine()) != null)
{
string[] lines = line.Split(",".ToCharArray());
object returnValue;
MyColumns object = new MyColumns();
object.Time = line[0];
object.System_Description = line[1];
object.User_Description = line[2];
///earlier in my code I have object of class called method
returnValue = method.MyMethod(mc.System_Description);
Class main = new Class();
main.Data1= object.Time;
main.ProblemData= returnValue;
main.Data2= object.User_Description;
list3.Add(main);
}
this.dataGridView3.DataSource = list3;
I have problem with showing ProblemData. Now in this column gridview shows me "project_name.Class_Name" (name of the class that this value was retured by)
EDIT:
Ok, I also have to mention that this class, from which returnValue gets values has 5 properties, let's say Categry, Name, Second_Name, Status and Value. This returnValue holds all this 5 properties with their current values.
EDIT2: Maybe someone knows how to display all this fields in one column? How can I join them only for displaying purpose? When I make normal List and insert this returnValue, it creates these 5 columns and insert values inside. Maybe it will make it easier to understand.
Please see my first comment on your question.
You have to use a nested GridView inside your second column which will bind to the returnValue. This is because GridView cannot automatically cascade your object datasource. The inner binding needs to be done in the RowDataBound event of your main GridView. For this to work, you will have to re-organise / re-factor your code.
Alternatively, you can concatenate the properties of the returnValue if their string representations can work for your scenario.
Edit:
The OP is asking about WinForms DataGridView (not ASP.Net):
The WinForms DataGridView does not support nesting out-of-the-box. However, there are some templating workarounds which are complicated. You are looking for a simple solution. I found one which can serve your immediate needs.
Hook into the CellFormatting event.
if (e.value is YOUR_OBJECT_TYPE) {
e.Value = (e.Value as YOUR_OBJECT_TYPE).YOUR_PROPERTY_NAME;
}
For details please refer to this: Binding to Nested Properties
Alternate option:
The alternate option of concatenating the properties of the returnValue as string, will also work.
main.ProblemData = "Cat: " + returnValue.Category + ", Name: " + returnValue.Name;
you should have defined your class variables like a propertiesbecause you are using them in databinding. like this..
public String Data1 {get;set;}
also make your list a ObservableCollection as it will notify the view whenever you change something in your list..
Two options
Override ToString() method in your ProblemData type
public class ProblemData
{
//whatever...
public override string ToString()
{
return string.format("{0}", this.SomeObject); //set proper display
}
}
public class YourClass()
{
//...
public ProblemData ProblemData{ get; set;}
}
Or you can set grid column formatter if object type can be formatted using string.Format
dataGridView3.Columns["ProblemData"].DefaultCellStyle.Format = "N";
//display string.Format({0:N}