DataGrid and Observable Collection in WPF - wpf

I have a datagrid like below in my WPF application.
<Window x:Class="MyApp.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<DataGrid x:Name="dgTest" ItemsSource="{Binding TestSource}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTemplateColumn Width="125" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Column1}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="500" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Column2}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Click="SaveButton_Click">Save</Button>
</Grid>
</Window>
I am binding it using the following code. Now my requirement is when the user enters some text into these textboxes inside datagrid and click on save button, it should update the database. How can I achieve this?
namespace MyApp
{
public partial class TestWindow: Window
{
private ObservableCollection<Test> _testSource
public ObservableCollection<Test> TestSource
{
get
{
return _testSource;
}
set
{
_testSource = value;
OnPropertyChanged("TestSource");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
public TestWindow()
{
InitializeComponent();
TestSource= new ObservableCollection<Test>();
string strConnString = Application.Current.Properties["connectionStr"].ToString();
SqlConnection con = new SqlConnection(strConnString);
SqlCommand cmd = new SqlCommand("SELECT Column1,Column2 FROM MyTable", con);
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataTable dtTest = new DataTable();
da.Fill(dtTest);
foreach (DataRow row in dtTest)
{
Test cd = new Test();
cd.Column1 = row["Column1"].ToString();
cd.Column2 = row["Column2"].ToString();
TestSource.Add(cd);
}
this.DataContext = this;
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
// here I need to get the updated ObservableCollection, but now it is showing old data
foreach Test t in TestSource)
{
string a = t.Column1;
string b = t.Column2;
}
}
}
public class Test:
{
public string Column1{ get; set; }
public string Column2{ get; set; }
}
}
Thanks

When creating your own UI in DataGridTemplateColumn (or a custom DataGrid.RowStyle for that matter), the DataGrid changes the UpdateSourceTrigger (i.e. when the underlying data should be updated) on all your bindings to Explicit if you didn't specify them yourself.
That "feature" is described briefly here: 5 Random Gotchas with the WPF DataGrid, and even though the author says this happens whether or not you set the UpdateSourceTrigger yourself, setting it yourself does actually work (at least in .Net 4.0).
Use LostFocus to mimic the default TextBox behavior (and PropertyChanged on CheckBox etc):
...
<TextBox Text="{Binding Column1, UpdateSourceTrigger=LostFocus}"></TextBox>
...
<TextBox Text="{Binding Column2, UpdateSourceTrigger=LostFocus}"></TextBox>
...

There are a couple of side notes I would like to make.
1) You dont need to have a setter on your TestSource property. You set this value once, and before the DataContext is set, so this is pointless.
2) You dont implement INotifyPropertyChanged on the Test class
3) You load the data on the UI thread. This can cause the App to freeze (become unresponsive) while the Data is being loaded.
Try updating the C# code to the below:
namespace MyApp
{
public partial class TestWindow: Window
{
private ObservableCollection<Test> _testSource = new ObservableCollection<Test>();
public TestWindow()
{
InitializeComponent();
//NOTE: this blocks the UI thread. Slow DB/Network will freeze the App while we wait.
// This should be done on a background thread.
string strConnString = Application.Current.Properties["connectionStr"].ToString();
SqlConnection con = new SqlConnection(strConnString);
SqlCommand cmd = new SqlCommand("SELECT Column1,Column2 FROM MyTable", con);
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataTable dtTest = new DataTable();
da.Fill(dtTest);
foreach (DataRow row in dtTest)
{
Test cd = new Test();
cd.Column1 = row["Column1"].ToString();
cd.Column2 = row["Column2"].ToString();
TestSource.Add(cd);
}
this.DataContext = this;
}
public ObservableCollection<Test> TestSource { get { return _testSource; } }
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
var rowIdx = 0;
foreach(var t in TestSource)
{
string a = t.Column1;
string b = t.Column2;
Console.WriteLine("Row {0}, col1='{1}', col2='{2}'", rowIdx++, a, b);
}
}
}
public sealed class Test : INotifyPropertyChanged
{
private string _column1;
private string _column2;
public string Column1
{
get{return _column1;}
set
{
if(_column1!=value)
{
_column1 = value;
OnPropertyChanged("Column1");
}
}
}
public string Column2
{
get{return _column2;}
set
{
if(_column2!=value)
{
_column2 = value;
OnPropertyChanged("Column2");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
}
}
You may also want to update the Binding to twoway, however I do think this is the default.
<TextBox Text="{Binding Column1, Mode=TwoWay}" />

Related

Issue with Combo box Item Template

Please find the below code.
public enum SortByType
{
[Display(Name = "Y Value")]
Y_Value,
[Display(Name = "X Value")]
X_Value,
[Display(Name = "Z Value")]
Z_Value,
}
public class ViewModel : INotifyPropertyChanged
{
ObservableCollection<SortByType> sortList;
SortByType selectedType;
public ViewModel()
{
SortList = new ObservableCollection<SortByType>(Enum.GetValues(typeof(SortByType)).OfType<SortByType>().ToList());
}
public ObservableCollection<SortByType> SortList
{
get
{
return sortList;
}
set
{
sortList = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SortList)));
}
}
public SortByType SelectedType
{
get
{
return selectedType;
}
set
{
selectedType = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedType)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new ViewModel();
(DataContext as ViewModel).SelectedType = SortByType.Y_Value;
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
(DataContext as ViewModel).SortList.Clear();
(DataContext as ViewModel).SortList = new ObservableCollection<SortByType>(Enum.GetValues(typeof(SortByType)).OfType<SortByType>().ToList());
}
}
<Grid>
<StackPanel VerticalAlignment="Center">
<ComboBox ItemsSource="{Binding SortList}"
SelectedItem="{Binding SelectedType}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Height="25" Width="150" Content="Details" Click="Button_Click"/>
</StackPanel>
</Grid>
Just, Clear the collection and re-initialize the combo box item source on button click event. On this time I have noticed that the Data error shown on the combo box. Can anyone explain what is the exact problem whether it may be a memory issue or some thing i have missed while implement the Item Template
The value of SelectedType must be present in the source collection. It's not when you assign the source property to a new list of new values, unless you set explicitly set it to any of these new values:
private void Button_Click(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as Win32ViewModel;
viewModel.SortList = new ObservableCollection<SortByType>(Enum.GetValues(typeof(SortByType)).OfType<SortByType>().ToList());
viewModel.SelectedType = viewModel.SortList.FirstOrDefault(x => x == viewModel.SelectedType);
}

how to make particular column of datagrid a combobox when AutoGenerateColumns="True"

I am new to MVVM . I am using wpf with MVVM in my project . So I am testing things right now before diving into an app I need to write.
My page (EmpDetailsWindow.xaml) is like this
<Grid>
<DataGrid Name="dgEmployee" Grid.Row="0" AutoGenerateColumns="True" ItemsSource="{Binding EmployeeDataTable}" CanUserAddRows="True" CanUserDeleteRows="True" IsReadOnly="False" />
<Button x:Name="btnSubmit" Content="Submit" Command="{Binding SubmitCommand}" CommandParameter="sample param" HorizontalAlignment="Left" Margin="212,215,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
and my model (EmpDetailsWindowViewModel) is as below
public class EmpDetailsWindowViewModel : INotifyPropertyChanged
{
public ICommand SubmitCommand { get; set; }
public EmpDetailsWindowViewModel()
{
EmployeeDataTable = DataTableCreator.EmployeeDataTable();
GenderDataTable = DataTableCreator.GenderDataTable();
SubmitCommand = new SubmitCommand();
}
DataTable _employeeDataTable;
public DataTable EmployeeDataTable
{
get { return _employeeDataTable;}
set
{
_employeeDataTable = value;
RaisePropertyChanged("EmployeeDataTable");
}
}
DataTable _genderDataTable;
public DataTable GenderDataTable
{
get { return _genderDataTable; }
set
{
_genderDataTable = value;
RaisePropertyChanged("GenderDataTable");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
datagrid is successfully bound to the datatable . Now I have a column "Gender" in datagrid. This should be a combobox and the item source of the cobobox is got from GenderDataTable of the view model . How can I achieve this ?
You can do it like this
<DataGrid AutoGeneratingColumn="DataGrid_AutoGeneratingColumn"/>
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "Gender")
{
var cb = new DataGridComboBoxColumn();
cb.ItemsSource = (DataContext as MyVM).GenderDataTable;
cb.SelectedValueBinding = new Binding("Gender");
e.Column = cb;
}
}
There doesn't seem to quite be a complete answer here so I'll post what I found from this question and from experimentation. I'm sure this breaks many rules but it's simple and it works
public partial class MainWindow : Window
{
// define a dictionary (key vaue pair). This is your drop down code/value
public static Dictionary<string, string>
dCopyType = new Dictionary<string, string>() {
{ "I", "Incr." },
{ "F", "Full" }
};
// If you autogenerate columns, you can use this event
// To selectively override each column
// You need to define this event on the grid in the event tab in order for it to be called
private void Entity_AutoGeneratingColumn(object sender,
DataGridAutoGeneratingColumnEventArgs e)
{
// The name of the database column
if (e.PropertyName == "CopyType")
{
// heavily based on code above
var cb = new DataGridComboBoxColumn();
cb.ItemsSource = dCopyType; // The dictionary defined above
cb.SelectedValuePath = "Key";
cb.DisplayMemberPath = "Value";
cb.Header = "Copy Type";
cb.SelectedValueBinding = new Binding("CopyType");
e.Column = cb;
}
}
} // end public partial class MainWindow

unable to bind the observablecollection to the datagrid in MVVM

am completely new to MVVM. I was trying to bind the data from sqlserver to the Datagrid in WPF and perform Edit, update and delete operations on it.
now, am unable to bind the data from sqlserver to datagrid atleast using observable collection in MVVM....
someone please help me out to resolve it and also kindly let me know how to implement the edit,update and delete operations for the same datagrid....am totally getting confused implementing the MVVM architecture..
with the following code I could bind the data to "Empdata"(observablecollection) but the datagrid is not being displayed at all.
the following is my xaml code :
<DataGrid ItemsSource="{Binding Path=Empdata}" x:Name="dtgrdemp"
AutoGenerateColumns="False"
SelectionMode="Single"
SelectionUnit="FullRow"
GridLinesVisibility="Horizontal"
CanUserDeleteRows="True"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Width="SizeToCells" MinWidth="125" Binding="{Binding Path=Ename}"/>
<DataGridTextColumn Header="Age" Width="SizeToCells" MinWidth="200" Binding="{Binding Path=Eage}"/>
<DataGridTextColumn Header="Description" Width="SizeToCells" MinWidth="200" Binding="{Binding Path=Edescription}"/>
</DataGrid.Columns></Datagrid>
the following is my code for "view" where i took a class as person
public class Person : INotifyPropertyChanged, IDataErrorInfo
{
private string names;
public string Names
{
get { return names; }
set
{
names = value;
OnPropertyChanged("Names");
}
}
private int age;
public int Age
{
get { return age; }
set
{
age = value;
OnPropertyChanged("Age");
}
}
private string description;
public string Description
{
get { return description; }
set
{
description = value;
OnPropertyChanged("Description");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
string error = null;
switch (columnName)
{
case "Names":
if (string.IsNullOrEmpty(names))
{
error = "First Name required";
}
break;
case "Age":
if ((age < 18) || (age > 85))
{
error = "Age out of range.";
}
break;
case "Description":
if (string.IsNullOrEmpty(description))
{
error = "Last Name required";
}
break;
}
return (error);
}
}
the following is my code for "ViewModel" class
public class MainViewModel :INotifyPropertyChanged
{
string con = ConfigurationSettings.AppSettings["ConnectionStrings"];
ObservableCollection<EmpInfo> Empdata= new ObservableCollection<EmpInfo>();
private Person empperson;
public Person Empperson
{
get { return empperson; }
set { empperson = value; }
}
public MainViewModel()
{
initializeload();
}
private void initializeload()
{
DataTable dt = new DataTable();
Empdata = new ObservableCollection<EmpInfo>();
Empdata.Add(new EmpInfo(dt));
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
}
the following is the code for EmpInfo.cs class
public EmpInfo(DataTable dt)
{
SqlConnection sqlcon = new SqlConnection(con);
sqlcon.Open();
SqlDataAdapter da = new SqlDataAdapter("Select Ename,Eage,Edescription from EmployeeTable", sqlcon);
da.Fill(dt);
this.dt = dt;
}
the following is the code in Appxaml.cs
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow();
var viewModel = new MainViewModel();
mainWindow.DataContext = viewModel;
mainWindow.Show();
}
Your code Empdata.Add(new EmpInfo(dt)); isn't populating the observable collection Empdata with anything!
Atleast thats what your constructor
public EmpInfo(DataTable dt)
confirms. Where is the code that populates the employee object by Age, Description and Name?
You can only DataBind Properties that are marked as PUBLIC.
Your Observable collection is internal.
You have to create a CollectionViewSource object and assign your ObservableCollection to its source property :
public CollectionViewSource datasource {get;set;}
...
datasource = new CollectionViewSource { Source = yourObservableCollection };
// you can do some sorting or grouping stuff here
your Binding should then look like:
<DataGrid ItemsSource="{Binding Path=datasource.View}" x:Name="dtgrdemp"

Mutually Exclusive comboboxes that binds to same data source - MVVM implementation

I'm not sure my Title is right but this is the problem I am facing now.. I have the below XAML code..
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=AvailableFields}"
SelectedItem="{Binding Path=SelectedField}"
></ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
What this basically does is, If my data source contains ten items, this is going to generate 10 row of comboboxes and all comboboxes are bounded to the same itemsource.
Now my requirement is Once an item is selected in the first combo box, that item should not be available in the subsequent combo boxes. How to satisfy this requirement in MVVM and WPF?
This turned out to be harder than I thought when I started coding it. Below sample does what you want. The comboboxes will contain all letters that are still available and not selected in another combobox.
XAML:
<Window x:Class="TestApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=SelectedLetters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=AvailableLetters}"
SelectedItem="{Binding Path=Letter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace TestApp
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
SelectedLetters = new List<LetterItem>();
for (int i = 0; i < 10; i++)
{
LetterItem letterItem = new LetterItem();
letterItem.PropertyChanged += OnLetterItemPropertyChanged;
SelectedLetters.Add(letterItem);
}
}
public List<LetterItem> SelectedLetters { get; private set; }
private void OnLetterItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "Letter")
{
return;
}
foreach (LetterItem letterItem in SelectedLetters)
{
letterItem.RefreshAvailableLetters(SelectedLetters);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public class LetterItem : INotifyPropertyChanged
{
static LetterItem()
{
_allLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Select(c => c.ToString());
}
public LetterItem()
{
AvailableLetters = _allLetters;
}
public void RefreshAvailableLetters(IEnumerable<LetterItem> letterItems)
{
AvailableLetters = _allLetters.Where(c => !letterItems.Any(li => li.Letter == c) || c == Letter);
}
private IEnumerable<string> _availableLetters;
public IEnumerable<string> AvailableLetters
{
get { return _availableLetters; }
private set
{
_availableLetters = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableLetters"));
}
}
}
private string _letter;
public string Letter
{
get { return _letter; }
set
{
if (_letter == value)
{
return;
}
_letter = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Letter"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private static readonly IEnumerable<string> _allLetters;
}
}
}
This functionality is not provided by WPF, but it can be implemented using some custom coding.
I've created 3 ViewModel classes:
PreferencesVM - This will be our DataContext. It contains the master list of options which can appear in the ComboBoxes, and also contains a SelectedOptions property, which keeps track of which items are selected in the various ComboBoxes. It also has a Preferences property, which we will bind our ItemsControl.ItemsSource to.
PreferenceVM - This represents one ComboBox. It has a SelectedOption property, which ComboBox.SelectedItem is bound to. It also has a reference to PreferencesVM, and a property named Options (ComboBox.ItemsSource is bound to this), which returns the Options on PreferencesVM via a filter which checks if the item may be displayed in the ComboBox.
OptionVM - Represents a row in the ComboBox.
The following points form the key to the solution:
When PreferenceVM.SelectedOption is set (ie a ComboBoxItem is selected), the item is added to the PreferencesVM.AllOptions collection.
PreferenceVM handles Preferences.SelectedItems.CollectionChanged, and triggers a refresh by raising PropertyChanged for the Options property.
PreferenceVM.Options uses a filter to decide which items to return - which only allows items which are not in PreferencesVM.SelectedOptions, unless they are the SelectedOption.
What I've described above might be enough to get you going, but to save you the headache I'll post my code below.
PreferencesVM.cs:
public class PreferencesVM
{
public PreferencesVM()
{
PreferenceVM pref1 = new PreferenceVM(this);
PreferenceVM pref2 = new PreferenceVM(this);
PreferenceVM pref3 = new PreferenceVM(this);
this._preferences.Add(pref1);
this._preferences.Add(pref2);
this._preferences.Add(pref3);
//Only three ComboBoxes, but you can add more here.
OptionVM optRed = new OptionVM("Red");
OptionVM optGreen = new OptionVM("Green");
OptionVM optBlue = new OptionVM("Blue");
_allOptions.Add(optRed);
_allOptions.Add(optGreen);
_allOptions.Add(optBlue);
}
private ObservableCollection<OptionVM> _selectedOptions =new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> SelectedOptions
{
get { return _selectedOptions; }
}
private ObservableCollection<OptionVM> _allOptions = new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> AllOptions
{
get { return _allOptions; }
}
private ObservableCollection<PreferenceVM> _preferences = new ObservableCollection<PreferenceVM>();
public ObservableCollection<PreferenceVM> Preferences
{
get { return _preferences; }
}
}
PreferenceVM.cs:
public class PreferenceVM:INotifyPropertyChanged
{
private PreferencesVM _preferencesVM;
public PreferenceVM(PreferencesVM preferencesVM)
{
_preferencesVM = preferencesVM;
_preferencesVM.SelectedOptions.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedOptions_CollectionChanged);
}
void SelectedOptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this,new PropertyChangedEventArgs("Options"));
}
private OptionVM _selectedOption;
public OptionVM SelectedOption
{
get { return _selectedOption; }
set
{
if (value == _selectedOption)
return;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Remove(_selectedOption);
_selectedOption = value;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Add(_selectedOption);
}
}
private ObservableCollection<OptionVM> _options = new ObservableCollection<OptionVM>();
public IEnumerable<OptionVM> Options
{
get { return _preferencesVM.AllOptions.Where(x=>Filter(x)); }
}
private bool Filter(OptionVM optVM)
{
if(optVM==_selectedOption)
return true;
if(_preferencesVM.SelectedOptions.Contains(optVM))
return false;
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
OptionVM.cs:
public class OptionVM
{
private string _name;
public string Name
{
get { return _name; }
}
public OptionVM(string name)
{
_name = name;
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new PreferencesVM();
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication64.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl ItemsSource="{Binding Path=Preferences}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Options}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedOption}"></ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
**Note that to reduce lines of code, my provided solution only generates 3 ComboBoxes (not 10).

WPF Datagrid / Datatable: Large number of rows

I have a Datagrid connected to Datatable, which needs to load a very large amount of rows.
To speed things up, I load 10% of the rows, and display the form. Most of the time the user only needs those 10% (they are the most recent entries). In a background thread I load the remaining 90% of the rows into another datatable (SecondData). Then I merge both datatables:
FirstData.BeginLoadData()
FirstData.Merge(SecondData, False)
FirstData.EndLoadData()
This works fine, but the Merge operation takes a very long time. If I reverse the operation (merging SecondData with FirstData), it takes much less time. But then I have to re-assign an itemsource (SecondData) to the Datagrid, and the user looses the current scrolling position, selected row, etc.
I also tried adding the rows directly to FirstData from the background thread, and it appears to work just fine. But when I scroll the Datagrid after that, I get freezes, and "DataTable internal index is corrupted", after that.
What would be the correct way of doing this?
Here is a somewhat hacked additional version that shows how to load a DataGrid when binding to a DataView using still BeginInvoke.
The code still loads one row at a time into the DataGrid.
You'll need to modify as needed; I am loading from the AdventureWorks sample using the Loaded event.
Here is how the ViewModel works:
First load the columns using a SQL statement with a Where clause of 1=0
Call Dispatcher.BeginInvoke to first load a subset of data
Then call Dispatcher.BeginInvoke again to load the remaining data
Here is the window:
<Window x:Class="DatagridBackgroundWorker.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfToolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Loaded="Window_Loaded"
Title="Main Window" Height="400" Width="800">
<DockPanel>
<Grid>
<WpfToolkit:DataGrid
Grid.Column="1"
SelectedItem="{Binding Path=SelectedGroup, Mode=TwoWay}"
ItemsSource="{Binding Path=GridData, Mode=OneWay}" >
</WpfToolkit:DataGrid>
</Grid>
</DockPanel>
</Window>
Here is the Window code-behind with the Loaded event:
public partial class MainView : Window
{
ViewModels.MainViewModel _mvm = new MainViewModel();
public MainView()
{
InitializeComponent();
this.DataContext = _mvm;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Dispatcher d = this.Dispatcher;
_mvm.LoadData(d);
}
}
Here the ViewModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
// load the connection string from the configuration files
_connectionString = ConfigurationManager.ConnectionStrings["AdventureWorks"].ConnectionString;
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
conn.Open();
// load no data 1=0, but get the columns...
string query =
"SELECT [BusinessEntityID],[Name],[SalesPersonID],[Demographics],[rowguid],[ModifiedDate] FROM [Sales].[Store] Where 1=0";
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;
cmd.CommandText = query;
SqlDataAdapter da = new SqlDataAdapter(cmd);
da.Fill(_ds);
}
}
// only show grid data after button pressed...
private DataSet _ds = new DataSet("MyDataSet");
public DataView GridData
{
get
{
return _ds.Tables[0].DefaultView;
}
}
private void AddRow(SqlDataReader reader)
{
DataRow row = _ds.Tables[0].NewRow();
for (int i = 0; i < reader.FieldCount; i++)
{
row[i] = reader[i];
}
_ds.Tables[0].Rows.Add(row);
}
public void LoadData(Dispatcher dispatcher)
{
// Execute a delegate to load the first number on the UI thread, with a priority of Background.
dispatcher.BeginInvoke(DispatcherPriority.Background, new LoadNumberDelegate(LoadNumber), dispatcher, true, 1);
}
// Declare a delegate to wrap the LoadNumber method
private delegate void LoadNumberDelegate(Dispatcher dispatcher, bool first, int id);
private void LoadNumber(Dispatcher dispatcher, bool first, int id)
{
try
{
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
conn.Open();
// load first 10 rows...
String query = string.Empty;
if (first)
{
// load first 10 rows
query =
"SELECT TOP 10 [BusinessEntityID],[Name],[SalesPersonID],[Demographics],[rowguid],[ModifiedDate] FROM [AdventureWorks2008].[Sales].[Store] ORDER By [BusinessEntityID]";
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;
cmd.CommandText = query;
int lastId = -1;
SqlDataReader reader = cmd.ExecuteReader();
if (reader != null)
{
if (reader.HasRows)
{
while (reader.Read())
{
lastId = (int)reader["BusinessEntityID"];
AddRow(reader);
}
}
reader.Close();
}
// Load the remaining, by executing this method recursively on
// the dispatcher queue, with a priority of Background.
dispatcher.BeginInvoke(DispatcherPriority.Background,
new LoadNumberDelegate(LoadNumber), dispatcher, false, lastId);
}
else
{
// load the remaining rows...
// SIMULATE DELAY....
Thread.Sleep(5000);
query = string.Format(
"SELECT [BusinessEntityID],[Name],[SalesPersonID],[Demographics],[rowguid],[ModifiedDate] FROM [Sales].[Store] Where [BusinessEntityID] > {0} ORDER By [BusinessEntityID]",
id);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;
cmd.CommandText = query;
SqlDataReader reader = cmd.ExecuteReader();
if (reader != null)
{
if (reader.HasRows)
{
while (reader.Read())
{
AddRow(reader);
}
}
reader.Close();
}
}
}
}
catch (SqlException ex)
{
}
}
private string _connectionString = string.Empty;
public string ConnectionString
{
get { return _connectionString; }
set
{
_connectionString = value;
OnPropertyChanged("ConnectionString");
}
}
}
If you you use the BeginInvoke method of a window or control’s Dispatcher property, it
adds the delegate to the Dispatcher’s event queue; however, you get the opportunity to specify a
lower priority for it. By executing a method that loads just one item at a time, the window is
given the opportunity to execute any other higher-priority events in between items. This allows the
control or window to be displayed and rendered immediately and loads each item one at a time.
Here is a some sample code that loads a ListBox.
You can adapt this to your DataGrid.
In this example I used a ViewModel that contains an ObservableCollection that contains a object.
If you have trouble converting to your DataGrid I'll rework.
Here is Window XAML:
<Window x:Class="ListBoxDragDrop.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Models="clr-namespace:ListBoxDragDrop.Models"
Loaded="Window_Loaded"
Title="Main Window" Height="400" Width="800">
<DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding Path=MyData}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Models:Person}">
<StackPanel>
<TextBlock Text="{Binding Name}" ></TextBlock>
<TextBlock Text="{Binding Description}" ></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DockPanel>
</Window>
Here is the Window code-behind with the Loaded event:
public partial class MainView : Window
{
MainViewModel _mwvm = new ViewModels.MainViewModel();
ObservableCollection<Person> _myData = new ObservableCollection<Person>();
public MainView()
{
InitializeComponent();
this.DataContext = _mwvm;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Execute a delegate to load
// the first number on the UI thread, with
// a priority of Background.
this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new LoadNumberDelegate(LoadNumber), 1);
}
// Declare a delegate to wrap the LoadNumber method
private delegate void LoadNumberDelegate(int number);
private void LoadNumber(int number)
{
// Add the number to the observable collection
// bound to the ListBox
Person p = new Person { Name = "Jeff - " + number.ToString(), Description = "not used for now"};
_mwvm.MyData.Add(p);
if (number < 10000)
{
// Load the next number, by executing this method
// recursively on the dispatcher queue, with
// a priority of Background.
//
this.Dispatcher.BeginInvoke(
DispatcherPriority.Background,
new LoadNumberDelegate(LoadNumber), ++number);
}
}
}
Here is the ViewModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
private ObservableCollection<Person> _myData = new ObservableCollection<Person>();
public ObservableCollection<Person> MyData
{
get
{
return _myData;
}
set
{
_myData = value;
OnPropertyChanged("MyData");
}
}
}
And the defintion of Person for completness:
public class Person
{
public string Name { get; set; }
public string Description { get; set; }
}

Resources