How to use DataContext? - wpf

What I am trying to do is simply have a combobox populate with data from an sqlite table. While I have done this with code methods, I wold really like to do this in what I can see is the better WPF way to do things.
From what I understand the flow should go something like this:
I should have a class that holds the data, I made a quick class which the default constructor is to connect to the database and dump it's results to a list like so:
internal class mainmenusql
{
private List<string> _Jobs;
public mainmenusql()
{
SQLiteConnection conn = new SQLiteConnection();
conn.ConnectionString = "Data Source=C:\\Users\\user\\Documents\\db.sqlite;Version=3";
try
{
conn.Open();
SQLiteDataReader reader;
SQLiteCommand command = new SQLiteCommand(conn);
command.CommandType = CommandType.Text;
command.CommandText = "SELECT * FROM Tasks";
reader = command.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
_Jobs.Add(reader.GetValue(0).ToString());
}
}
else
{
MessageBox.Show("No records");
}
}
catch (Exception err)
{
MessageBox.Show(err.Message);
}
finally
{
conn.Close();
}
}
}
Having some errors with the list "Object reference not set to an instance of an object".
But anyways, the next step should be to set the DataContext of the form to this object right?
public MainWindow()
{
DataContext = new mainmenusql();
InitializeComponent();
}
And finally the combobox should have a binding right?
<Window x:Class="sqliteDatacontext.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>
<ComboBox DataContext="{Binding Path=_Jobs}" HorizontalAlignment="Left" Margin="141,124,0,0" VerticalAlignment="Top" Width="120"/>
</Grid>
</Window>
What am I doing wrong here?

For binding to something of a datacontext, it needs to be exposed via public getter / setter...
public class mainmenusql
{
public List<string> _Jobs
{ get ; protected set; }
public mainmenusql()
{
_Jobs = new List<string>();
// rest of populating your data
}
}
The binding in your window control is the ItemsSource
<ComboBox ItemsSource="{Binding Path=_Jobs}" />
The "DataContext" is applied to the entire window... From that being the basis, any of your controls can have their element "bound" to almost anything "publicly" available ON your data context... in this case, a ComboBox list of choices comes from its "ItemSource" property... So you want the ITEMSOURCE pointing to your _Jobs.

Related

Listbox doesn't show the contents of an observablecollection

I have made an application with the mvvm model and a sql database connected to azure. When I try to get information in the database my listbox detects how many objects are in the collection and shows the path to the List instead of the content.
I have tried my sql query and this just gives back the data fine.
view:
xmlns:local="clr-namespace:Lance_Theunis_r0702301_2ITFA"
xmlns:viewmodel="clr-
namespace:Lance_Theunis_r0702301_2ITFA.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<viewmodel:WagenViewModel x:Key="WagenViewModel" />
</Window.Resources>
<DockPanel LastChildFill="True" DataContext="{StaticResource
WagenViewModel}">
<StackPanel DockPanel.Dock="Left" Width="333px">
<ListBox ItemsSource="{Binding Path=Wagens}" />
</StackPanel>
viewmodel:
namespace Lance_Theunis_r0702301_2ITFA.ViewModel
{
class WagenViewModel : BaseViewModel
{
public WagenViewModel()
{
LeesWagens();
}
private ObservableCollection<Wagen> wagens;
public ObservableCollection<Wagen> Wagens
{
get
{
return wagens;
}
set
{
wagens = value;
NotifyPropertyChanged();
}
}
private void LeesWagens()
{
DataService wagenDS = new DataService();
wagens = new ObservableCollection<Wagen>(wagenDS.GetWagens());
}
}
}
DataService class:
namespace Lance_Theunis_r0702301_2ITFA.Model
{
class DataService
{
private static string connectionString =
ConfigurationManager.ConnectionStrings["azure"].ConnectionString;
private static IDbConnection db = new SqlConnection(connectionString);
public List<Wagen> GetWagens()
{
string sql = "Select naam from Wagen order by naam";
return (List<Wagen>)db.Query<Wagen>(sql);
}
}
}
There are no error messages.
The listbox shows (Lance_Theunis_r0702301_2ITFA.Model.Wagen) instead of for example (bmw m3).
Set the DisplayMemberPath property to "naam" or whatever the name of the property of the Wagen class that you want to display is:
<ListBox ItemsSource="{Binding Path=Wagens}" DisplayMemberPath="naam" />

Combobox is not refreshing in WPF using MVVM

I have a problem similar to How to get an ItemsSource to refresh its bind?
But I did used the INotifyPropertyChange interface and still having the issue. Here is the XAML:
<UserControl x:Class="Sample.Module.Pages.View.ModifyDataTypeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:Dialog="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
xmlns:local="clr-namespace:Sample.Module.Pages.ViewModel"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="350">
<UserControl.DataContext>
<local:DataTypeViewModel/>
</UserControl.DataContext>
<Canvas>
<ComboBox Canvas.Left="0" Canvas.Top="0"
Margin="13,22,0,0"
Style="{DynamicResource VirtualisedMetroComboBox}"
Controls:TextBoxHelper.Watermark="Autocompletion"
DisplayMemberPath="DataTypeName"
IsEditable="True"
ItemsSource="{Binding DataTypes}"
MaxDropDownHeight="125"
SelectedItem="{Binding Path=SelectedDataType, Mode=TwoWay}" Height="25"/>
</Canvas>
Here is the ViewModel:
public class DataTypeViewModel : DomainObject
{
private ObservableCollection<DataTypeRepository> dataTypes;
private DataTypeRepository selectedDataType;
private DataTypeModel dataTypeModel;
public DataTypeViewModel()
{
dataTypeModel = new DataTypeModel();
selectedDataType = new DataTypeRepository();
this.dataTypes = dataTypeModel.GetAllDataTypes();
InsertDataTypeCommand = new DelegateCommand(OnInsertDataType);
}
public ObservableCollection<DataTypeRepository> DataTypes
{
get { return dataTypes; }
set
{
if (!EqualityComparer<ObservableCollection<DataTypeRepository>>.Default.Equals(dataTypes,value))
{
dataTypes = value;
RaisePropertyChanged("DataTypes");
}
}
}
public DataTypeRepository SelectedDataType
{
get
{
return selectedDataType;
}
set
{
if (!EqualityComparer<DataTypeRepository>.Default.Equals(selectedDataType, value))
{
selectedDataType = value;
InsertDataTypeCommand.RaiseCanExecuteChanged();
RaisePropertyChanged("SelectedDataType");
}
}
}
public DelegateCommand InsertDataTypeCommand { get; private set; }
private void OnInsertDataType()
{
DataBaseOperationStatusMessage = dataTypeModel.InsertDataType(selectedDataType);
DatabaseOperationComplete = true;
DataTypes = dataTypeModel.GetAllDataTypes();
}
}
Please note the InsertDataTypeCommand command is used in another another tab of the app where the "DataType" is added. After adding a DataType user clicks to the Modify Tab to see the new DataType in the combobox list. But that is not happenning. If you restart the app and go to the Modify page you can see the new record.
The issue is the Combobox is not getting updated even when am using INotifyPropertyChange.
What am I missing here?
Try updating your binding to include the update trigger...
SelectedItem="{Binding Path=SelectedDataType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
I would guess that the following line isn't causing the collection to be updated:
DataTypes = dataTypeModel.GetAllDataTypes();
From your code it looks like you go and insert the new item into your database or wherever, and are then running the method that loads all the items from the database. Instead of running that method it would be better, when you are inserting into the database, to create a corresponding DataTypeRepository item and add that to the collection.
this.DataTypes.Add(new DataTypeRepository { property1= "somedata"});
It just means if you're adding a new item onto what could be a huge list of items, you won't have to reload everything.
You should probably also update your itemsource to be 2 way binding (unless you're doing the readonly that Will suggested in the comments) and put the updatetrigger to property changed (by default its on focus lost i think)

Window freezes when adding items to ObservableCollection

I have a DataGrid which is bound to an ObservableCollection ProductsFound
which is exposed as a property in my ViewModel.
By typing text in a TextBox, products contained in the model that have the Code property that contains the text inserted in the TextBox are added to ProductsFound.
I found out that if the DataGrid is contained in any control such as a StackPanel or a TabItem, the Window (the program) stops responding when I try to type text into the TextBox; while if the DataGrid isn't contained in any control, everything runs normally.
Here's the code for the window:
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// This method just fill the dataset I pass to the model's contructor in the next line.
Init();
ProductsModel model = new ProductsModel(dataSet);
searchViewModel = new ProductsSearchViewModel(model);
DataContext = searchViewModel;
}
private ProductsSearchViewModel searchViewModel;
// This handler supports the binding between the TextBox and the MatchText property of the View Model.
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var binding = ((TextBox)sender).GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
}
}
And here's my ViewModel:
public class ProductsSearchViewModel : Notifier, IProductsSearchViewModel
{
public ProductsSearchViewModel(IProductsModel inModel)
{
model = inModel;
productsFound = new ObservableCollection<ProductViewModel>();
}
private string matchText;
private IProductsModel model;
private ObservableCollection<ProductViewModel> productsFound;
// This is a helper method that search for the products in the model and adds them to ProductsFound.
private void Search(string text)
{
Results.Clear();
foreach (Product product in model.Products)
{
if (product.Code.ToLower().Contains(text.ToLower()))
Results.Add(new ProductViewModel(product));
}
}
public string MatchText
{
get { return matchText; }
// This setter is meant to be executed every time the Text property of the TextBox is changed.
set
{
if ((value != matchText) && (value != ""))
{
matchText = value;
// This raises INotifyPropertyChanged.PropertyChaged.
NotifyPropertyChanged("MatchText");
Search(value);
}
}
}
public ObservableCollection<ProductViewModel> ProductsFound
{
get
{
return productsFound;
}
set
{
productsFound = value;
NotifyPropertyChanged("Results");
}
}
}
Here's the XAML:
<Window x:Class="MyNameSpace.UI.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>
<StackPanel>
<TextBox Text="{Binding MatchText, Mode=TwoWay}" TextChanged="TextBox_TextChanged" />
<DataGrid x:Name="grid1" ItemsSource="{Binding Results}" >
</StackPanel>
</Grid>
With that StackPanel the program stops responding when I try to type text in the Textbox and no item is added to the DataGrid; but if i remove it everything runs ok.
What could the problem be? Am I missing something in how the WPF binding system works?
Is my view model coded wrong?
Thanks in advance.
Putting that StackPanel there prevents the DataGrid from acquiring a specific Height, thus it just expands down to infinity, and that breaks UI Virtualization.
Remove the StackPanel from there and use a non-infinite container, such as Grid or DockPanel.

MVVM gridview binding to datatable WPF

I am new to MVVM and databinding and I am having some trouble binding a gridview to a datatable dynamically. I am able to get the column headers to bind, but no data is being displayed in the grid itself.
My model simply returns a data table as the result of a SQL string passed to it.
My viewmodel just wraps the datatable and gets bound to the view.
Right now I am just trying to display the data by populating the gridview from the main window, but only the headers are being displayed.
I know there is data in the model.Results datatable though.
My viewmodel:
public class ResultsViewModel
{
private DataTable _dt;
public ResultsViewModel()
{
DataSource _ds = new DataSource();
_dt = _ds.Execute("select * from tbl_users");
}
public DataTable Results
{
get { return _dt; }
set { _dt = value; }
}
}
My code to populate the gridview from the mainwindow:
public MainWindow()
{
InitializeComponent();
ResultsView view = new ResultsView();
ResultsViewModel model = new ResultsViewModel();
GridView Grid = new GridView();
foreach (DataColumn col in model.Results.Columns)
{
Grid.Columns.Add(new GridViewColumn
{
Header = col.ColumnName,
DisplayMemberBinding = new Binding(col.ColumnName)
});
}
view._listView.View = Grid;
view.DataContext = model;
view.SetBinding(ListView.ItemsSourceProperty, new Binding());
_placeholder.Content = view;
}
The ResultsView xaml:
<UserControl x:Class="InDevReporting.Views.ResultsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ListView x:Name="_listView" />
</Grid>
Try setting your data context to model.Results.
ie change this line:
view.DataContext = model;
to this:
view.DataContext = model.Results;
Generally you would create a dependency property on your view model and specify the binding in the XAML. The grid should be clever enough to figure out what columns to draw:
<ListView ItemsSource="{Binding Results}" />
public MainWindow()
{
InitializeComponent();
// your code to instance and populate model
this.DataContext = model;
}
public class ResultsViewModel : DependencyObject
{
public static readonly DependencyProperty ResultsProperty = DependencyProperty.Register("Results", typeof(DataTable) , typeof(ResultsViewModel));
public DataTable Results
{
get { (DataTable)GetValue(ResultsProperty); }
set { SetValue(ResultsProperty, value); }
}
}
I've tapped this out from memory, so apologies if the code isn't exactly right. The easiest way to declare a new dependency property is to use the propdp code snippet. It's a lot of syntax to memorize.

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