I have two datagrid displayed on my UI. When I select a particular row on datagrid 1, I would like to display the details of the datagrid 1 on datagrid 2. I am populating the datagrid data from a database.
here is the two database table structure.
Note: both the table are mapped by the personid in the database
here is the code so far I have tried
Baseclass.cs
public class Baseclass
{
public event PropertyChangedEventHandler PropertyChanged;
protected void SetProperty<T>(ref T member, T value, [CallerMemberName] string propertyName = null)
{
member = value;
this.RaiseNotification(propertyName);
}
protected void RaiseNotification(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
person.cs
public class person : Baseclass
{
private int personID;
public int PersonID
{
get { return personID; }
set { this.SetProperty<int>(ref this.personID, value); }
}
private string firstName;
public string FirstName
{
get { return firstName; }
set { this.SetProperty<string>(ref this.firstName, value); }
}
private string lastName;
public string LastName
{
get { return lastName; }
set { this.SetProperty<string>(ref this.lastName, value); }
}
Model _personModel = new Model();
private ObservableCollection<person> _person = new ObservableCollection<person>();
public ObservableCollection<person> Getpersons
{
get { return _person; }
set { _person = value; OnPropertyChanged("GetPersons"); }
}
public person()
{
initializeload();
}
private void initializeload()
{
try
{
DataTable table = _personModel.getData();
for (int i = 0; i < table.Rows.Count; ++i)
Getpersons.Add(new person
{
PersonID = Convert.ToInt32(table.Rows[i][0]),
FirstName = table.Rows[i][1].ToString(),
LastName = table.Rows[i][2].ToString(),
});
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
public class Model
{
public DataTable getData()
{
DataTable ndt = new DataTable();
SqlConnection sqlcon = new SqlConnection(ConfigurationManager.ConnectionStrings["MyConnectionString"].ConnectionString);
sqlcon.Open();
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [Person].[dbo].[persons]", sqlcon);
da.Fill(ndt);
da.Dispose();
sqlcon.Close();
return ndt;
}
}
}
PersonDetail class
public class PersonDetails : Baseclass
{
private int personID;
public int PersonID
{
get { return personID; }
set { this.SetProperty<int>(ref this.personID, value); }
}
private string address;
public string Address
{
get { return address; }
set { this.SetProperty<string>(ref this.address, value); }
}
private string pos;
public string Position
{
get { return pos; }
set { this.SetProperty<string>(ref this.pos, value); }
}
DetailsModel _detailModel = new DetailsModel();
private ObservableCollection<PersonDetails> _details = new ObservableCollection<PersonDetails>();
public ObservableCollection<PersonDetails> GetDetails
{
get { return _details; }
set { _details = value; OnPropertyChanged("GetDetails"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
public PersonDetails()
{
initializeload();
}
private void initializeload()
{
try
{
DataTable table = _detailModel.getData();
for (int i = 0; i < table.Rows.Count; ++i)
GetDetails.Add(new PersonDetails
{
PersonID = Convert.ToInt32(table.Rows[i][0]),
Address = table.Rows[i][1].ToString(),
Position = table.Rows[i][2].ToString(),
});
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public class DetailsModel
{
public DataTable getData()
{
DataTable ndt = new DataTable();
SqlConnection sqlcon = new SqlConnection(ConfigurationManager.ConnectionStrings["MyConnectionString"].ConnectionString);
sqlcon.Open();
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [Person].[dbo].[personDetails]", sqlcon);
da.Fill(ndt);
da.Dispose();
sqlcon.Close();
return ndt;
}
}
}
MainViewModel.cs
public class MainViewModel : Base, INotifyPropertyChanged
{
public MainViewModel()
{
}
private ObservableCollection<person> personValues;
public ObservableCollection<person> Persons
{
get { return personValues; }
set
{
this.SetProperty<ObservableCollection<person>>(ref this.personValues, value);
}
}
private ObservableCollection<PersonDetails> detailsValues;
public ObservableCollection<PersonDetails> Details
{
/* This is correct below ?? I have an error as
'PersonDemo.MainViewModel' does not contain a definition for 'GetDetails' and no extension method 'GetDetails' accepting a first argument of type 'PersonDemo.MainViewModel' could be found (are you missing a using directive or an assembly reference?)*/
get { return this.GetDetails(this.Selectedperson.PersonID); }
}
private person selectedValue;
public person Selectedperson
{
get { return selectedValue; }
set
{
this.SetProperty<person>(ref this.selectedValue, value);
this.RaiseNotification("Details");
}
}
}
XAML
<Grid>
<DataGrid Margin="100,20,116,211" ItemsSource="{Binding Persons}" SelectedItem="{Binding Selectedperson}" />
<DataGrid Margin="100,130,116,101" ItemsSource="{Binding Details}" />
</Grid>
can anybody help to proceed in writing the MainViewModel? I am stuck here since weeks.
First, I recomend to use the INotifyPropertyChanged interface in the base class. Maybe you use it and forget write it in the sample code. For make this with MVVM patter, you need to implement a ViewModel for both data grids. Let's call it in our example BothGridViewModel, you may call it as you want. Then in this view model you need the collection of all person, let's call it AllPerson, then you need to have a property of Person type, when you will have the selected person in the grid, let's call it SelectedPerson. It will be something like this:
public class BothGridViewModel:INotifyPropertyChanged
{
...
public ObservableCollection<Person> AllPersons {...}
public Person SelectedPerson {...}
...
}
All you need, is to set it in the DataContext of the View. And bindings:
<YourView DataContext={Binding SomeBothGridViewModelClass}>
<Grid>
<DataGrid Margin="100,20,116,211" ItemsSource="{Binding AllPersons}" SelectedItem="{Binding SelectedPerson}" />
<DataGrid Margin="100,130,116,101" ItemsSource="{Binding SelectedPerson.Getpersons}" /> <!--Or some SelectedPerson.PersonDetails.GetDetails-->
</Grid>
</YourView DataContext={Binding SomeBothGridViewModelClass}>
I think this is the good view model structure for make what you want. Hope this helps you...
EDIT
I see your point now, you have two database tables, one for the main properties and other for the details. I see two good ways for doing this:
1) The first one, is that I don't belive that the second datagrid is necesary due for each person you don't have a collection of details. Instead you may use a grid, and other controls for showing the properties. Also I think you must to implement a view model for the person, for instance:
public class PersonViewModel:INotifyPropertyChanged
{
public string FirstName {...}
public string LastName {...}
//Other properties
public PersonDetails Details {...}
}
Then in the grid, you may bind the items source to a collection of PersonViewModel then you can make bindings to the selected item of the grid, for instance:
<Grid>
<DataGrid x:Name="dataGrid" ItemsSource={Binding AllPersonViewModels}/>
...
<!--Then some combo box, text block or text box binding to a property of the selected item's details-->
...
<TextBox Text={Binding SelectedItem.Details.Address, ElementName=dataGrid}/>
...
<Grid>
2) The second way I think could be done, is showing all data in the same datagrid. For this you need to do the PersonViewModel class in this way:
public class PersonViewModel:INotifyPropertyChanged
{
public string FirstName {...}
public string LastName {...}
//Other properties
//Then the Details properties
public string Address {...}
//...
//Note this class is like a wrapper for person, and person details
}
This way is a bit simpler, but maybe cause an unwanted database over access.
EDIT 2
After have a look of your code, I have to say a few things about it: In the PersonViewModel and PersonDetailViewModel you should use a DispatcherTimer instead a Timer, because we are in Wpf. Other thing, maybe you should use a Repository pattern, to create a PersonRepository and a PersonDetailRepository where to put all the DB comunication, in fact, PersonViewModel and PersonDetailViewModel are a in some way, repositories, but by now you don't need to change it, it should work. I'm going to show you here the code of the MainViewModel, I nodified it to have the SelectedPersonDetail property, in this way, all you need to do is make a binding in the View:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using tryout13022013.PersonViewModels;
using System.ComponentModel;
using tryout13022013.DetailsViewModel;
using tryout13022013.PersonModel;
namespace tryout13022013
{
public class MainViewModel
{
private PersonViewModel _subPerson = new PersonViewModel();
public PersonViewModel SubPerson
{
get
{
return _subPerson;
}
set
{
if (_subPerson != value)
{
_subPerson = value; OnPropertyChanged("SubPerson");
}
}
}
private PersonDetailsViewModel _subDetail = new PersonDetailsViewModel();
public PersonDetailsViewModel SubDetail
{
get { return _subDetail; }
set
{
_subDetail = value; OnPropertyChanged("SubDetail");
}
}
private Person _selectedPerson;
public Person SelectedPerson
{
get { return _selectedPerson; }
set {
if (_selectedPerson != value)
{
_selectedPerson = value;
OnPropertyChanged("SelectedPerson");
OnPropertyChanged("SelectedPersonDetail"); //In this way when Change the Selected Person, the Selected Detail will be changed again...
//if (this.SelectedPerson != null && this.SubDetail != null)
//{
// I dont know how to call the PersonDetailsViewModel class like a method here in order to load its data. kindly help
//this.SubDetail.MethodforLoadingPersonDetails(this.SelectedPerson);
//}
}
}
}
public PersonDetails SelectedPersonDetail
{
get
{
if (SubDetail == null || SelectedPerson ==null)
return null;
return SubDetails.DetailsData.FirstOrDefault(detail => detail.PersonID == SelectedPerson.PersonID);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
}
}
And this is an instace of a binding you can make in your View, in this case for selecting the item in the second grid:
<Window x:Class="tryout13022013.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:person="clr-namespace:tryout13022013.PersonViewModels"
xmlns:details="clr-namespace:tryout13022013.DetailsViewModel"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid ItemsSource="{Binding SubPerson.PersonData}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}"
AutoGenerateColumns="False" Height="77" HorizontalAlignment="Left"
Margin="101,26,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="auto" >
<DataGrid.DataContext>
<person:PersonViewModel/>
</DataGrid.DataContext>
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Width="auto" Binding="{Binding PersonID}"/>
<DataGridTextColumn Header="First Name" Width="auto" Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name" Width="auto" Binding="{Binding LastName}"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource="{Binding SubDetail.DetailsData}"
AutoGenerateColumns="False" Height="77" HorizontalAlignment="Left"
Margin="101,135,0,0" Name="dataGrid2" VerticalAlignment="Top" Width="255" SelectedItem="{Binding SelectedPersonDetail, Mode=OneWayToSource}">
<DataGrid.DataContext>
<details:PersonDetailsViewModel/>
</DataGrid.DataContext>
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Width="auto" Binding="{Binding PersonID}"/>
<DataGridTextColumn Header="Address" Width="auto" Binding="{Binding Address}"/>
<DataGridTextColumn Header="Position" Width="auto" Binding="{Binding Description}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
I forgot it, you need to make the binding to SubPerson.PersonData and SubDetail.DetailsData, these are the collection.
Check it out...
Having had a chat with Buba1947 in WPF room, I've concluded that she's actually got a 1-1 relation split table in database that she's actually trying to show as such in the split data grids.
I've proposed her to create a PersonViewModel on top of Person & PersonDetails tables so her MainViewModel contains only one ObservableCollection<PersonViewModel> under the Persons property to which we shall bind the two DataGrids.
so MainViewModel.cs (the DataContext) has:
private ObservableCollection<PersonViewModel> personValues;
public ObservableCollection<PersonViewModel> Persons
{
get { return personValues; }
set { this.SetProperty<ObservableCollection<PersonViewModel>>(ref this.personValues, value); }
}
The data adaptor is changed to bring the data in using an inner join with relevant query being something along the lines of this (which fills the Persons property above):
"SELECT * FROM [Person].[dbo].[persons] INNER JOIN [Person].[dbo].[personDetails] ON [Person].[dbo].[persons].[Id] = [Person].[dbo].[personDetails].[Id]"
Next we shall bind a CollectionViewSource to Persons in Xaml:
<Window.Resources>
<CollectionViewSource x:Key="PersonsData" Source={Binding Persons} />
</Window.Resources>
and bind DataGrids to this with individual columns:
<DataGrid ItemsSource={Binding Source={StaticResource PersonsData}} AutoGenerateColumns="false">
<DataGrid.Columns>
<DataGridTextColumn Header="Person Id" Content={Binding PersonId} />
<DataGridTextColumn Header="First Name" Content={Binding FirstName} />
<DataGridTextColumn Header="Last Name" Content={Binding LastName} />
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource={Binding Source={StaticResource PersonsData}} AutoGenerateColumns="false">
<DataGrid.Columns>
<DataGridTextColumn Header="Person Id" Content={Binding PersonId} />
<DataGridTextColumn Header="Address" Content={Binding Address} />
<DataGridTextColumn Header="Position" Content={Binding Position} />
</DataGrid.Columns>
</DataGrid>
Note: there may be typo in here as I haven't got VS to hand, please let me know if I need to fix something. I've written all of this from memory.
Related
So I got a WPF datagrid which is created like this:
<DataGrid x:Name="columns" Grid.Row="1" HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding}" DataContext="{StaticResource ColumnsCollection}"
AutoGenerateColumns="False" CanUserAddRows="True" CanUserDeleteRows="True" AddingNewItem="columns_AddingNewItem">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="Auto"/>
<DataGridTextColumn Header="Width of Name" Binding="{Binding WidthOfNameInCentimeter}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
it is initialized like this:
CollectionViewSource itemCollectionViewSource;
itemCollectionViewSource = (CollectionViewSource)(FindResource("ColumnsCollection"));
itemCollectionViewSource.Source = new ObservableCollection<FormCreatorColumnInfoDatasource>();
and populated.
The FormCreatorColumnInfoDatasource looks like this:
class FormCreatorColumnInfoDatasource : INotifyPropertyChanged
{
private IFormCreatorColumnInfo m_info;
public string Name
{
get
{
return m_info.Name;
}
set
{
m_info.Name = value;
}
}
public string WidthOfNameInCentimeter
{
get
{
var exactWidthInCentimeter = Name.Length * 0.238M;
var roundedUpToHalfCentimeter = Math.Round(exactWidthInCentimeter * 2, MidpointRounding.AwayFromZero) / 2;
return (roundedUpToHalfCentimeter).ToString();
}
}
}
So now I want to instantly change the content of the column WidthOfNameInCentimeter (Width of Name) when the user manually changes the Name of the column so there is always the correct information in this column.
How do I do this?
With the help of #lidqy I added a function and changed the setter of Name:
class FormCreatorColumnInfoDatasource : INotifyPropertyChanged
{
private IFormCreatorColumnInfo m_info;
public string Name
{
get
{
return m_info.Name;
}
set
{
m_info.Name = value;
OnPropertyChanged(nameof(Name));
OnPropertyChanged(nameof(WidthOfNameInCentimeter));
}
}
public string WidthOfNameInCentimeter
{
get
{
var exactWidthInCentimeter = Name.Length * 0.238M;
var roundedUpToHalfCentimeter = Math.Round(exactWidthInCentimeter * 2, MidpointRounding.AwayFromZero) / 2;
return (roundedUpToHalfCentimeter).ToString();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Dont forget to implement INotifyPropertyChanged in the datasource and to use an ObservableCollection for the datagrids source.
I am using mvvm Light and what I want is to add/delete some rows in my datagrid. The initial values are displayed, but when I add some info my collection is populated with the values I entered, but its type is GalaSoft.MvvmLight.ObservableObject and not "MyType" and because of this when I want to delete a new added row my application craches (SelectedItem is null)...(if I want to delete a row that was not added manually it works!).
<DataGrid Name="Table" ItemsSource="{Binding MyCollection}" SelectedItem="{Binding SelectedItem}" IsSynchronizedWithCurrentItem="True">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Width="*" Binding="{Binding Path=Id,Mode=TwoWay}" />
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Path=Name,Mode=TwoWay}" />
</DataGrid.Columns>
in viewModel:
public MyType SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged();
}
}
public ObservableCollection<ObservableObject> MyCollection
{
get
{
return _myCollection;
}
set
{
_myCollection = value;
RaisePropertyChanged();
}
}
public RelayCommand RemoveRow { get { return new RelayCommand(RemoveRowCommand, CanRemove); } }
public RelayCommand AddRow { get { return new RelayCommand(AddRowCommand, CanAdd); } }
public void RemoveRowCommand()
{
MyCollection.Remove(SelectedItem);
}
public void AddRowCommand()
{
MyCollection.Add(SelectedItem);
}
Make your class inherit BindableBase.
Make property lik this
private ObservableColletion<ObservableObject> _myCollection;
public ObservableCollection<ObservableObject> MyCollection
{
get
{
return _myCollection;
}
set
{
SetProperty(ref _myCollection,new ObservableCollection<ObservableObject>(value));
}
}
SetProperty method (from BindableBase class) takes care about raising property change events. It should works :)
I'm binding DataGridCoulmun's DisplayIndex to my ViewModel. Since DataGridColumns doesn't belong to DataGrid's visual or lgical tree, i had to do some some tricks to achieve that binding, but it works.
My problem is: When DataContext is changed (if we have more ViewModels), DataGridColumns shoud get new DisplayIndexes. Unfortunately the behaviour is strange and after the change, the column order is more or less random.
Do you have any idea how to handle this problem or at least what is the cause?
Here is a example:
Before initializing the datagrid I set the DataContext to the new instance of the ViewModel and it works as it should. After that i reorder the columns and it still works and the changes are propagated to the ViewModel correctly. Finally I click the button, which set the DataContext to the new instance of the ViewModel, so the columns after click should be ordered as at the beginning.
Here is a XAML code:
<Window x:Class="TestDataGridBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestDataGridBinding"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn
Header="A"
Binding="{Binding A}"
DisplayIndex="{Binding Path=Data.ADisplayIndex, FallbackValue=0, Mode=TwoWay, Source={StaticResource proxy}}"/>
<DataGridTextColumn
Header="B"
Binding="{Binding B}"
DisplayIndex="{Binding Path=Data.BDisplayIndex, FallbackValue=1, Mode=TwoWay, Source={StaticResource proxy}}"/>
<DataGridTextColumn
Header="C"
Binding="{Binding C}"
DisplayIndex="{Binding Path=Data.CDisplayIndex, FallbackValue=2, Mode=TwoWay, Source={StaticResource proxy}}"/>
<DataGridTextColumn
Header="D"
Binding="{Binding D}"
DisplayIndex="{Binding Path=Data.DDisplayIndex, FallbackValue=3, Mode=TwoWay, Source={StaticResource proxy}}"/>
</DataGrid.Columns>
</DataGrid>
<Button Click="Button_Click" Width="70" Height="30" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="10" Content="Click me"/>
</Grid>
</Window>
Here is a Code-behind
public partial class MainWindow : Window
{
ViewModel _model;
public MainWindow()
{
_model = new ViewModel();
_model.Items.Add(new Item("x","y","z","zz"));
DataContext = _model;
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_model = new ViewModel();
_model.Items.Add(new Item("xx", "y", "zz", "zzz"));
DataContext = _model;
}
}
[Serializable]
public class ViewModel : INotifyPropertyChanged
{
#region notifications
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
#endregion
#region private and default values
private int _a = 3;
private int _b = 2;
private int _c = 1;
private int _d = 0;
private ObservableCollection<Item> _items = new ObservableCollection<Item>();
#endregion
#region public
public int ADisplayIndex { get { return _a; } set { _a = value; NotifyPropertyChanged("ADisplayIndex"); } }
public int BDisplayIndex { get { return _b; } set { _b = value; NotifyPropertyChanged("BDisplayIndex"); } }
public int CDisplayIndex { get { return _c; } set { _c = value; NotifyPropertyChanged("CDisplayIndex"); } }
public int DDisplayIndex { get { return _d; } set { _d = value; NotifyPropertyChanged("DDisplayIndex"); } }
public ObservableCollection<Item> Items
{ get { return _items; } set { _items = value; NotifyPropertyChanged("Items"); } }
#endregion
}
public class Item
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public Item(string a, string b, string c, string d)
{
A = a; B = b; C = c; D = d;
}
}
public class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Trick with proxy was found here:
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
The DataGrid automatically renumbers the DisplayIndex of all the other columns when you adjust the first.
E.g. if you set your second column to DisplayIndex = 0, it will give the first column DisplayIndex = 1.
See UpdateDisplayIndexForChangedColumn() in https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/DataGridColumnCollection.cs
This is going to play havoc with your bindings unless you change them in ascending order.
You'd need some kind of custom NotifyPropertyChanged logic which sets all of the new desired index values on your model first, without raising NotifyPropertyChanged, then raises NotifyPropertyChanged events in ascending numerical order.
eg.
_aDisplayIndex = 2;
_bDisplayIndex = 1;
_cDisplayIndex = 0;
_dDisplayIndex = 3;
NotifyPropertyChanged("CDisplayIndex");
NotifyPropertyChanged("BDisplayIndex");
NotifyPropertyChanged("ADisplayIndex");
NotifyPropertyChanged("DDisplayIndex");
Also watch out for two-way binding, in this case you might want to re-write the values of _*DisplayIndex between each NotifyPropertyChanged call.
My first post here! And new to coding!...
I have a WPF Datagrid in a Frame on a Page. I would like to click (preferably single click) on a row and use the ID value stored in one of the columns to Navigate to (open) a new Page.
Using MouseDoubleClick sometimes I can double click row to open a Page. But sometimes it is throwing:
"An unhandled exception of type 'System.NullReferenceException' occurred in program.exe
Additional information: Object reference not set to an instance of an object."
on line (see code behind below for complete method):
string ID = ((DataRowView)PersonDataGrid.SelectedItem).Row["PersonID"].ToString();
XAML:
<DataGrid x:Name="PersonDataGrid" AutoGenerateColumns="False"
SelectionMode="Single" SelectionUnit ="FullRow"
MouseDoubleClick="PersonDataGrid_CellClicked" >
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=PersonID}"
ClipboardContentBinding="{x:Null}" Header="ID" />
</DataGrid.Columns>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell" BasedOn="{StaticResource myDataGridCellStyle}">
<EventSetter Event="DataGridCell.MouseLeftButtonDown" Handler="PersonDataGrid_CellClicked"/>
</Style>
</DataGrid.CellStyle>
</DataGrid>
Code Behind:
private void PersonDataGrid_CellClicked(object sender, MouseButtonEventArgs e)
{
string ID = ((DataRowView)PersonDataGrid.SelectedItem).Row["PersonID"].ToString();
SelectedPersonID = Int32.Parse(ID);
this.NavigationService.Navigate(new PersonProfile());
}
Is there a better way to open the PersonProfile Page? Is there an easy way to open the Page using single click on a row?
Thanks.
A better way to do it, is to define a collection of Person that define the DataGrid ItemSource and a Property of type Person that contains the selected Item in the DataGrid:
<DataGrid x:Name="PersonDataGrid" AutoGenerateColumns="False"
SelectionMode="Single" SelectionUnit ="FullRow"
MouseRightButtonUp="PersonDataGrid_CellClicked"
ItemsSource="{Binding Persons}"
SelectedItem="{Binding SelectedPerson}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=PersonId}" Header="Id" />
<DataGridTextColumn Binding="{Binding Path=PersonName}" Header="Name" />
</DataGrid.Columns>
</DataGrid>
and the SelectedPerson and Persons are defined in the code behind of this page like so :
public partial class Page1 : Page,INotifyPropertyChanged
{
private ObservableCollection<Person> _persons ;
public ObservableCollection<Person> Persons
{
get
{
return _persons;
}
set
{
if (_persons == value)
{
return;
}
_persons = value;
OnPropertyChanged();
}
}
private Person _selectedPerson ;
public Person SelectedPerson
{
get
{
return _selectedPerson;
}
set
{
if (_selectedPerson == value)
{
return;
}
_selectedPerson = value;
OnPropertyChanged();
}
}
public Page1()
{
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Person
{
public int PersonId { get; set; }
public string PersonName { get; set; }
}
the INotifyPropertyChanged is used to notify the UI of any changes in the Properties.
You could use the SelectedPerson Property or pass it to the new page, when you receive the MouseRightButtonUp event for example in the DataGrid:
private void PersonDataGrid_CellClicked(object sender, MouseButtonEventArgs e)
{
if (SelectedPerson == null)
return;
this.NavigationService.Navigate(new PersonProfile(SelectedPerson));
}
and you could change the PersonProfile Page to receive a Person in its Constructor.
The exception you are getting is because the SelectedItem is sometimes null. To make sure you are getting the value you want use the sender object and cast it to the proper type. As for a better way I would look into the MVVM pattern. It would involve binding the SelectedItem to an object. This might be a little much because you are new but it's what I recommend.
In my application I want to validate if the user enter an item which is already exist on the DataGrid when I enter a new Item on the cell. I validate my business object using IDataErrorInfo.
My object is as follows:
class clsProducts : INotifyPropertyChanged, IDataErrorInfo
{
private string _ProductName;
private decimal _PurchaseRate;
private int _AvailableQty;
private int _Qty;
private decimal _Amount;
#region Property Getters and Setters
public string ProductName
{
get { return _ProductName; }
set
{
if (_ProductName != value)
{
_ProductName = value;
OnPropertyChanged("ProductName");
}
}
}
public decimal PurchaseRate
{
get { return _PurchaseRate; }
set
{
_PurchaseRate = value;
OnPropertyChanged("PurchaseRate");
}
}
public int AvailableQty
{
get { return _AvailableQty; }
set
{
_AvailableQty = value;
OnPropertyChanged("AvailableQty");
}
}
public int Qty
{
get { return _Qty; }
set
{
_Qty = value;
this._Amount = this._Qty * this._PurchaseRate;
OnPropertyChanged("Qty");
OnPropertyChanged("Amount");
}
}
public decimal Amount
{
get { return _Amount; }
set
{
_Amount = value;
OnPropertyChanged("Amount");
}
}
#endregion
#region IDataErrorInfo Members
public string Error
{
get
{
StringBuilder error = new StringBuilder();
// iterate over all of the properties
// of this object - aggregating any validation errors
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(this);
foreach (PropertyDescriptor prop in props)
{
string propertyError = this[prop.Name];
if (!string.IsNullOrEmpty(propertyError))
{
error.Append((error.Length != 0 ? ", " : "") + propertyError);
}
}
return error.ToString();
}
}
public string this[string name]
{
get
{
string result = null;
if (name == "ProductName")
{
if (this._ProductName != null)
{
int count = Global.ItemExist(this._ProductName);
if (count == 0)
{
result = "Invalid Product "+this._ProductName;
}
}
}
else if (name == "Qty")
{
if (this._Qty > this._AvailableQty)
{
result = "Qty must be less than Available Qty . Avaialble Qty : " + this._AvailableQty;
}
}
return result;
}
}
#endregion
#region INotifyPropertyChanged Members
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
//// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
My xaml is :
<my:DataGrid Name="dgReceiveInventory" RowStyle="{StaticResource RowStyle}" ItemsSource="{Binding}" GotFocus="dgReceiveInventory_GotFocus" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False" RowHeight="23" SelectionUnit="Cell" AutoGenerateColumns="False" Margin="12,84,10,52" BeginningEdit="dgReceiveInventory_BeginningEdit">
<my:DataGrid.Columns>
<!--0-Product Column-->
<my:DataGridTemplateColumn Header="Product Name" Width="200">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Style="{StaticResource TextBlockInError}" Text="{Binding ProductName,ValidatesOnDataErrors=True}" ></TextBlock>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
<my:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox x:Name="txtbxProduct" Style="{StaticResource TextBoxInError}" Text="{Binding Path=ProductName,UpdateSourceTrigger=LostFocus,ValidatesOnDataErrors=True}" TextChanged="txtbxProduct_TextChanged" PreviewKeyDown="txtbxProduct_PreviewKeyDown" >
</TextBox>
</DataTemplate>
</my:DataGridTemplateColumn.CellEditingTemplate>
</my:DataGridTemplateColumn>
<!--1-Purchase Rate Column-->
<my:DataGridTextColumn Header="Purchase Rate" Width="100" Binding="{Binding PurchaseRate}" IsReadOnly="True"></my:DataGridTextColumn>
<!--2-Avaialable Qty Column-->
<my:DataGridTextColumn Header="AvailableQty" Binding="{Binding AvailableQty}" IsReadOnly="True" Visibility="Hidden"></my:DataGridTextColumn>
<!--3-Qty Column-->
<my:DataGridTextColumn Header="Qty" Binding="{Binding Qty,ValidatesOnExceptions=True,ValidatesOnDataErrors=True}" EditingElementStyle="{StaticResource TextBoxInError}">
</my:DataGridTextColumn>
<!--4-Amount Column-->
<my:DataGridTextColumn Header="Amount" Width="100" Binding="{Binding Amount}" ></my:DataGridTextColumn>
</my:DataGrid.Columns>
</my:DataGrid>
Now I want to show the user ,if he made a duplicate entry in the datagrid cell how to do this ?
You can't do this functionality in your model or data type class using the IDataErrorInfo interface because you won't have access to the other objects there. Instead, you'll have to do it in your view model. However, you can report the error using that interface. I have extended its functionality by adding a property into my data type base class:
public virtual ObservableCollection<string> ExternalErrors
{
get { return externalErrors; }
}
As you can see, mine deals with multiple errors, but you can easily change this to:
public virtual string ExternalError
{
get { return externalError; }
}
Then I 'plug' this into my Errors property:
public override ObservableCollection<string> Errors
{
get
{
errors = new ObservableCollection<string>();
errors.AddUniqueIfNotEmpty(this["Name"]);
errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
errors.AddUniqueIfNotEmpty(this["StatementPrefixes"]);
errors.AddRange(ExternalErrors);
return errors;
}
}
Again, I have adapted this interface to return multiple errors, but you can change this to:
public override string Error
{
get
{
error = string.Empty;
if ((error = this["Name"])) != string.Empty) return error;
if ((error = this["EmailAddresses"])) != string.Empty) return error;
if ((error = this["Name"])) != string.Empty) return error;
if (ExternalError != string.Empty) return ExternalError;
return error;
}
}
Incidentally, it is far more efficient to call just the indexers that you are actually validating rather than your example of calling all properties using reflection. However, that is your choice.
So now that we have this ExternalError property, we can use it to display external error messages from your view model (create a class that contains a collection property to bind to the DataGrid.ItemsSource property).
If you are using ICommand objects, then you can put this code into the CanExecute method of your Save command:
public bool CanSave(object parameter)
{
clsProducts instance = (clsProducts)parameter;
instance.ExternalError = YourCollectionProperty.Contains(instance) ?
"The values must be unique" : string.Error;
// Perform your can save checks here
}
Please note that you will need to implement the Equals method in your data type object for this to work. There are many similar ways to achieve this and I trust that from this example you will be able to work one out that works for you.