I'm currently trying to populate a ListView with data imported by the user from a CSV file. As such, I know nothing of the number of columns nor their names at time of compilation, which is why I create a GridView and its columns in code and set it as the ListView's View property.
Normally the ListView supports virtualization when the data is bound, but I can't bind it due to the aforementioned problem. And the fact remains that the view needs virtualization as I expect the user to be importing CSVs with tens of thousands of lines.
As I'm quite new to WPF and MVVM in general, I might be thinking about this problem the wrong way, but I haven't found any practical solution to my problem on here or anywhere. I'm thinking it might be the way I'm representing the data bound to the GridView, which is a custom class with the data being arrays, like this
public class DataSet
{
public double[] Data { get; private set; }
public double[] Targets { get; private set; }
...
}
and binding that in code like this
for (int i = 0; i < numDataSets; i++)
{
gridView.Columns.Add( new GridViewColumn()
{
Header = "N" + (i + 1),
Width = 40d,
DisplayMemberBinding = new Binding("Targets[" + i + "]")
});
}
A second ListView is used to display the Data array variable of the DataSet class, to avoid any confusion.
EDIT
This is the new ViewModel I'm currently using as the data provider of sorts. This model is bound to the DataGrids ItemSource property, and when new data is inbound, a new DataTable is created from it and set to the ViewModels property
public class DataModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged (string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName)); // error thrown here
}
private System.Data.DataTable data;
public System.Data.DataTable Data
{
get { return data; }
set
{
if (data != value)
{
data = value;
OnPropertyChanged("Data");
}
}
}
}
Switch your ListView for a DataGrid and use its AutoGenerateColumns property.
Also, any formatting you need to do to your data collection, do it in your ViewModel before consuming it from the View.
<DataGrid ItemsSource="{Binding Data}"
AutoGenerateColumns="True"
EnableColumnVirtualization="True"
EnableRowVirtualization="True" />
You don't need to define your columns anywhere, they'll be auto-generated based on the columns of your DataTable. And with the last two properties, you activate virtualization so your RAM and processor don't get clogged.
EDIT - Make sure your DataGrid uses its own ScrollViewer. If you put it inside another ScrollViewer or inside a StackPanel, it won't be able to virtualize correctly.
Related
I am still baffled finding out how to get a DataGridView updated automatically when changing the content of its DataSource without explicitely triggering DataGridView.Update(). It seems there is no difference at all between DataTable, List, BindingList as (direct) DataSource and as (indirect) DataSource with an additional BindingSource which uses any of the former as DataSource.
The DataGridView I am actually using for this is non-editable and just shows entries which are updated by the corresponding entity code. My last attempt was with a BindingSource that uses a BindingList and manipulating the content of the BindingSource within the code.
I have omitted some methods here, which do not play a role for the basic problem.
Form:
private void FormLog_Load(object sender, EventArgs e) {
...
dgvLog.DataSource = Log.Current.SourceEntries;
...
}
private void ClearLog() {
Log.Current.RemoveAll();
}
public void UpdateDataSource() {
dgvLog.Update();
}
Entity (singleton class):
public class LogEntry {
public int ID { get; set; }
public string DateTime { get; set; }
public string Type { get; set; }
public string Event { get; set; }
public string Details { get; set; }
}
public class Log {
public BindingList<LogEntry> Entries { get; set; }
public BindingSource SourceEntries { get; set; }
public Log() {
Entries = new BindingList<LogEntry>();
SourceEntries = new BindingSource() { DataSource = Entries };
ReadAll();
}
public void Add(string type, string logEvent, string details = "") {
LogEntry entry = MapToDB(new LogEntry() {
Type = type,
Event = logEvent,
Details = details
});
DB.Write(QueryAdd(entry));
SourceEntries.Add(entry);
if (Config.Current.GetForm("Log") != null)
((FormLog)Config.Current.GetForm("Log")).UpdateDataSource();
}
public void ReadAll() {
for (int i = SourceEntries.Count - 1; i >= 0; i--) {
SourceEntries.RemoveAt(i);
}
DataTable dt = DB.Read(QueryReadAll());
if (dt != null) {
foreach (DataRow row in dt.Rows) {
SourceEntries.Add(MapToList(row));
}
}
if (Config.Current.GetForm("Log") != null)
((FormLog)Config.Current.GetForm("Log")).UpdateDataSource();
}
public void RemoveAll() {
DB.Write(QueryRemoveAll());
for (int i = SourceEntries.Count - 1; i >= 0; i--) {
SourceEntries.RemoveAt(i);
}
Add("I", "Log cleared");
}
This works but only when I call UpdateSource() which calls dgvLog.Update() by using a selfwritten FormStack in another singleton class which I would like to avoid. Of course, one could simply call dgvLog.Update() within the form itself but, esp. with this log example, it is obvious that this does not help when updating data from/within another form while the form that displays the DataGridView is still opened in the background.
Also, as there is no difference (between using DataTable or List, etc. and BindingSource or not) I wonder what the benefit/purpose of BindingList and BindingSource are:
Is this the correct approach or am I missing something!?
By the way, I am using .NET v4.5.2.
It seems there is no difference at all between DataTable, List, BindingList as (direct) DataSource and as (indirect) DataSource with an additional BindingSource which uses any of the former as DataSource.
A BindingSource has a few uses
maintains knowledge of position/current row and can thus achieve shared navigation (a dgv and textboxes all bound to the same BS means the dgv can navigate through records and the textboxes update because they always show "current row")
provides sorting and filtering facilities
supports complex binding scenarios where it must help filter a list down to only children of some currently selected parent in a different bindingsource
provides separation for multi different positional browsing of a common DataSource
works but only when I call UpdateSource() which calls dgvLog.Update() by using a selfwritten FormStack in another singleton class which I would like to avoid. Of course, one could simply call dgvLog.Update() within the form itself but, esp. with this log example, it is obvious that this does not help when updating data from/within another form while the form that displays the DataGridView is still opened in the background.
Datagridview.Update() is concerned with repainting areas for the control that need it; it is nothing to do with committing changes to underlying data models. Perhaps you need EndEdit which finishes editing operations on the current row and commits them to the underlying data storage. This also happens when you click a different row in a grid. Bindingsource also have an EndEdit method. Mostly you don't need to call these methods yourself
To share data between forms pass the datatable the data is stored in and bind it through a bindingsource in the second form
Also, as there is no difference (between using DataTable or List, etc. and BindingSource or not) I wonder what the benefit/purpose of BindingList and BindingSource are:
DataTable is a collection of DataRow. A DataRow is at it's heart an object array. The end
Binding list is a list of whatever you want, such as your custom class Person. Arguably more useful ultimately, but it's comparing apple's and oranges. If instead you open up the DataSet Designer then you can specify tables that have named typed column. In that regard it's not a huge amount different from writing your own classes (it writes a large amount of good code on a short time though. I use them as data models sometimes
I got to have one question about the auto generating column function of wpf data grid.
As you may already know, when we use a data table as an itemsource like
this.datagrid.ItemsSource = table.AsDataView();
we can see the data grid make columns according to table's columns.
But When I use my own list of data model, I get stuck.
My model class is like below
public class MyDataGridModel
{
private object[] _itemArray;
public object[] ItemArray
{
get { return _itemArray; }
set { _itemArray = value; }
}
public MyDataGridModel(List<string> data)
{
ItemArray = data.ToArray();
}
}
Populating a data grid with a list of MyDataGridModel doesn't work.
It just make one column named "ItemArray", not many columns as much as number of data in ItemArray
So, My question is that there is any interface or some magic method to make my model work like data table.
Update
It seemed that there was a unclear thing on my question.
I tried like below and xaml for a datagrid only set AutoGenerateColumn property to true.
For DataTable
List<string> data = new List<string>() { "a", "b", "c" };
DataTable table = new DataTable();
table.Columns.Add("col1");
table.Columns.Add("col2");
table.Columns.Add("col3");
table.Rows.Add(data.ToArray());
table.Rows.Add(data.ToArray());
table.Rows.Add(data.ToArray());
this.datagrid.ItemsSource = table.AsDataView();
then the the datagrid showed that three columns each named coll,2,3 and three rows that each has a,b,c
For List of MyDataGridModel
List<string> data = new List<string>() { "a", "b", "c" };
List<MyDataGridModel> data2 = new List<MyDataGridModel>();
data2.Add(new MyDataGridModel(data));
data2.Add(new MyDataGridModel(data));
data2.Add(new MyDataGridModel(data));
this.datagrid.ItemsSource = data2;
then the datagrid showed only one column named ItemArray and three rows which have string "String[] Array".
I know that I can see all data in MyDataGridModel if I add some column configuration to a datagrid. But I want to see all data without any specific column configuration, which makes a datagrid be able to use for any length of ItemArray.
I even hope I get to know if it is impossible thing or not
Thanks in advance
Your ViewModel should be inheriting from INotifyPropertyChanged
Create a new class file called ViewModelBase
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Be sure to include the using statements
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
Then in your ViewModel
public class MyDataGridModel : ViewModelBase
{
private List<string> _items;
public List<string> Items
{
get => _items;
set => Set(ref _items, value);
}
public MyDataGridModel(List<string> data)
{
Items = data;
}
}
Then in your View set the ItemSource to bind to your Items List, be sure to set the DataContext in your view to the ViewModel
<Window.DataContext>
<local:MyDataGridModel/>
</Window.DataContext>
<DataGrid ItemsSource="{Binding Items, UpdateSourceTrigger=PropertyChanged}" />
You are only going to get 1 Column as your data type is just of string if you want to display more columns you will need to make a Model with all your properties you wish to display in the datagrid and set the type of string to your Model name then create columns in the datagrid for each property and bind their values to each column.
Hope this is of some help to you.
Edit:
Turns out I was merging a list with the latest data from a rest API with the data in my GUI. All I really had to do was clear and refill my observablecollection. This post is basically an xy problem. I did not have the vocabulary to explain the problem I was facing.
I'm building an app with a Data class where I store all my data. In this class I have a List filled with objects. I have a Page with a ObservableCollection and a ListView. Currently when I update the ObservableCollection, I clear it and refill it with all the data from Data-class's List. When I do this, my ListView flickers. My guess is that completely rebuilding the ObservableCollection causes this, in combination with a custom ViewCell that is not the lightests. How could I go about updating only what I want? The list/o.collection can have diffrent sizes. The list/o.collection both store the same object.
What I tried:
List<csharp.Truck> favs = data.getFavoriteTrucks();
trucks.Clear();
foreach (csharp.Truck truck in favs)
{
trucks.Add(truck);
}
}
Works but makes my ListView flicker.
Trying this now, its pretty bad code I think, it does update the list how I want it to but the listview does not get updated for some reason. Maybe I need to trigger a refresh?
List<csharp.Truck> all = data.getTrucks();
//if list sizes are not equal(excess/missing objects)
if (all.Count != trucks.Count)
{
//excess object
if (all.Count < trucks.Count)
{
trucks.Clear();
foreach(csharp.Truck t in all)
{
trucks.Add(t);
}
}
//object missing
if(all.Count > trucks.Count)
{
foreach(csharp.Truck t in all)
{
if (!trucks.Contains(t))
{
trucks.Add(t);
}
}
}
}//objects are now the same
//test if object is present but outdated(favorite property)
foreach(csharp.Truck t in trucks)
{
if (t.isFavorite() != all[all.IndexOf(t)].isFavorite())
{
t.setFavorite(all[all.IndexOf(t)].isFavorite());
}
}
Also let me know if this approach is not good practise in the first place.
If you want to update only some properties, you could implement in your model class INotifyPropertyChanged interface.
public class Model : INotifyPropertyChanged
{
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set
{
_myProperty = value;
RaisePropertyChanged();
}
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = "")
{
Volatile.Read(ref PropertyChanged)?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And then if this property is binded to view, view will know, when you change it:
<Label Text="{Binding MyProperty}" />
I've been trying to essentially replace the whole table that is auto generated by the DataGrid control with a new DataTable.
I looked at this question before, but I've been trying to avoid referencing UI elements in my viewmodel class (or adding code to the Mainwindow.xaml.cs as a last resort).
First I bound the ItemsSource for a DataGrid to a DataView property I have in my Viewmodel:
<DataGrid Grid.Row="1" Grid.Column ="1" ItemsSource="{Binding DisplayView}"/>
private DataView displayView;
public DataView DisplayView
{
get { return displayView; }
set { displayView = value; }
}
I also have a method that formats an SQL table into an appropriate DataTable, which I then assign to the DataView bound to the grid.
DisplayView = dtCloned.DefaultView;
When I call this method in the constructor, the table fills up on execution.
Calling it during runtime creates a new table in the same format, but assigning it to the DataView property doesn't reload the DataGrid to show the new table; where am I going wrong?
For bindings to be aware of any updates on the properties behind you must implement the INotifyPropertyChanged interface on your viewmodel/model.
Once you do so, your code should look like this:
public class YourViewModel : INotifyPropertyChanged
{
...
public event PropertyChangedEventHandler PropertyChanged;
private DataView displayView;
public DataView DisplayView
{
get { return displayView; }
set
{
displayView = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(nameof(DisplayView)));
}
}
}
...
}
More information on the interface found here.
I'm trying to bind a WPF DataGrid to a List<ClassName>.
Below is my DataGrid:
<DataGrid ItemsSource="{Binding Source=FileProcessing}" AutoGenerateColumns="True"></DataGrid>
Below I am binding the list with database records:
FileProcessing = GetFileProcessingInfo(dtDateStart, dtDateEnd);
The FileProcessing is defined as a property below:
public List<FileProcessing_T> FileProcessing { get; set; }
The GetFileProcessingInfo Method also returns a List<FileProcessing_T> object.
The FileProcessing list does get some records from the database but the grid does not bind the data from the list.
I will appreciate your help.
You can keep your databinding.
But your have to implement the INotfifyPropertyChanged interface in the class where the FileProcessing property is located.
Because in the setter of FileProcessing you have to perform the change notification.
public ObservableCollection<FileProcessing_T> FileProcessing
{
get
{
return _fileProcessing;
}
set
{
if (_fileProcessing != value)
{
_fileProcessing = value;
RaisePropertyChanged("FileProcessing");
}
}
}
ObservableCollection<FileProcessing_T> _fileProcessing;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Otherwise the UI control will not know (not be notified) that the bound data has changed.
This will be enough to fix your problem.
It would even work if you continued to use List<FileProcessing_T> instead of ObservableCollection<FileProcessing_T>, however the ObservableCollection also supports change notifications if single elements are added and removed from the collection while List does not.