Datagrid validation to prevent duplicate entry - wpf

Using the code below I can trap an invalid cell entry. In this simple example of a grocery shopping list, the GroceryItem.Name just needs to be filled in.
What I would like to do now is add the ability to validate that the entry doesn't already exist. If it does already exist then I'd like the same cell entry highlighted but with the appropriate message. So if the user enters "Eggs" again, the cell error message should be "Eggs is already on the list".
The item view model shouldn't know about it's container view model, so where can I check for a duplicate entry while still at cell validation for the "Name" property?
Item in the Collection
public class GroceryItem : ObservableObject, IDataErrorInfo
{
#region Properties
/// <summary>The name of the grocery item.</summary>
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChangedEvent("Name");
}
}
private string _name;
#endregion
#region Implementation of IDataErrorInfo
public string this[string columnName] {
get {
if (columnName == "Name") {
if (string.IsNullOrEmpty(Name))
return "The name of the item to buy must be entered";
}
return string.Empty;
}
}
public string Error { get { ... } }
#endregion
}
View Model holding the Collection
public class MainWindowViewModel : ViewModelBase
{
/// <summary>A grocery list.</summary>
public ObservableCollection<GroceryItem> GroceryList
{
get { return _groceryList; }
set
{
_groceryList = value;
RaisePropertyChangedEvent("GroceryList");
}
}
private ObservableCollection<GroceryItem> _groceryList;
/// <summary>The currently-selected grocery item.</summary>
public GroceryItem SelectedItem { get; [UsedImplicitly] set; }
void OnGroceryListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// doing non-validation stuff
}
}
View Binding for DataGrid
<DataGrid
x:Name="MainGrid"
ItemsSource="{Binding GroceryList}"
SelectedItem="{Binding SelectedItem}"
...
>
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="TextBlock.ToolTip"
Value="{Binding Error}" />
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
...
<DataGridTextColumn Header="Item" Width="*" Binding="{Binding Name, ValidatesOnDataErrors=True}" IsReadOnly="false" />
</DataGrid.Columns>
</DataGrid>

I don't know if this violates MVVM but the way I would do it is pass GroceryList to GroceryItem in another constructor and save it in a private ObservableCollection groceryList in GroceryItem. It is just a reverence back to GroceryList so it does not add a much overhead.
public class GroceryItem : ObservableObject, IDataErrorInfo
{
private ObservableCollection<GroceryItem> groceryList;
public void GroceryItem(string Name, ObservableCollection<GroceryItem> GroceryList)
{
name = Name;
groceryList = GroceryList;
// now you can use groceryList in validation
}
}

Related

WPF, Update ComboBox ItemsSource when it's DataContext changes

I have two classes A and B which both implement an interface IThingWithList.
public interface IThingWithList
{
ObservableCollection<int> TheList;
}
TheList in A contains 1, 2, 3
TheList in B contains 4, 5, 6
I have a controller class which has a list of IThingWithList which contains A and B
public class MyControllerClass
{
public ObservableCollection<IThingWithList> Things { get; } = new ObservableCollection<IThingWithList>() { A, B };
public IThingWithList SelectedThing { get; set; }
}
Now, in xaml I have two ComboBoxes as follows
<ComboBox
ItemsSource="{Binding MyController.Things}"
SelectedValue="{Binding MyController.SelectedThing, Mode=TwoWay}" />
<ComboBox
DataContext="{Binding MyController.SelectedThing}"
ItemsSource="{Binding TheList}" />
The first ComboBox controls which (A or B) is the data context of the second combo box.
Problem:
When I select A or B from the first ComboBox The list items of the second ComboBox are not updated.
What I have tried:
Making both A and B ObservableObjects
Making IThingWithList implement INotifyPropertyChanged
Adding UpdateSourceTrigger to the ItemsSource Bindings
Scouring Google.
Here is how I typically do the ViewModel (in your case "Controller") Base Class in order to get the functionality you are looking for:
This is the base class that all VMs derive from.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetAndNotify<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(property, value))return;
property = value;
this.OnPropertyChanged(propertyName);
}
}
Here is how I would adjust your ControllerClass:
public class MyControllerClass : ViewModelBase
{
private ObservableCollection<IThingWithList> _things;
public ObservableCollection<IThingWithList> Things
{
get => _things;
set { SetAndNotify(ref _things, value); }
}
private IThingWithList _selectedthing;
public IThingWithList SelectedThing
{
get => _selectedThing;
set{SetAndNotify(ref _selectedThing, value);}
}
}
Now adjust your XAML to have the DataContext of the container set instead of each Control (makes life easier)
<UserControl xmlns:local="clr-namespace:YourMainNamespace">
<UserControl.DataContext>
<local:MyControllerClass/>
</UserControl.DataContext>
<StackPanel>
<ComboBox
ItemsSource="{Binding Things}"
SelectedValue="{Binding SelectedThing, Mode=TwoWay}" />
<!-- ComboBox ItemsSource="See next lines" /-->
</StackPanel>
</Window>
You can change SelectedThing to be an ObservableCollection or you can have a second object that is the list and updates accordingly:
//Add into MyControllerClass
public MyInnerThingList => SelectedThing.TheList;
//Edit the SelectedThing to look like:
private IThingWithList _selectedthing;
public IThingWithList SelectedThing
{
get => _selectedThing;
set
{
SetAndNotify(ref _selectedThing, value);
RaisePropertyChanged(nameof(MyInnerThingList));
}
}
Then change the binding to:
<ComboBox ItemsSource="{Binding MyInnerThingList, Mode=OneWay}" />
You may also want to add a SelectedMyInnerThing property, but not sure if that is needed.

TreeView MVVM, how to get selection?

I am trying to create a TreeView for my application. This is the first time I am using TreeView with the MVVM structure, by all accounts the binding is working and displaying correctly.
However:
How do I get the selection so I can perform some logic after the user selects something?
I thought that the TextValue property in SubSection class would fire PropertyChanged, but it doesn't, so I am left scratching my head.
This is the most simplified set of code I could make for this question:
Using PropertyChanged setup like this in : ViewModelBase class
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The VeiwModel:
public class ShiftManagerViewModel : ViewModelBase
{
public ShiftManagerViewModel()
{
Departments = new List<Department>()
{
new Section("Section One"),
new Section("Section Two")
};
}
private List<Section> _sections;
public List<Section> Sections
{
get{return _sections;}
set
{
_sections = value;
NotifyPropertyChanged();
}
}
}
The classes:
public class Section : ViewModelBase
{
public Section(string depname)
{
DepartmentName = depname;
Courses = new List<SubSection>()
{
new SubSection("SubSection One"),
new SubSection("SubSection One")
};
}
private List<SubSection> _courses;
public List<SubSection> Courses
{
get{ return _courses; }
set
{
_courses = value;
NotifyPropertyChanged();
}
}
public string DepartmentName { get; set; }
}
public class SubSection : ViewModelBase
{
public SubSection(string coursename)
{
CourseName = coursename;
}
public string CourseName { get; set; }
private string _vTextValue;
public string TextValue
{
get { return _vTextValue; }
set
{
_vTextValue = value;
NotifyPropertyChanged();
}
}
}
And the XAML:
<Window.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Courses}" DataType="{x:Type viewModels:Section}">
<Label Content="{Binding DepartmentName}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding TextValue}" DataType="{x:Type viewModels:SubSection}">
<Label Content="{Binding CourseName}" />
</HierarchicalDataTemplate>
</Window.Resources>
Could someone point me in the right direction?
You could cast the SelectedItem property of the TreeView to a Section or a SubSection or whatever the type of the selected item is:
Section section = treeView1.SelectedItem as Section;
if (section != null)
{
//A Section is selected. Access any of its properties here
string name = section.DepartmentName;
}
else
{
SubSection ss = treeView1.SelectedItem as SubSection;
if(ss != null)
{
string ssName = ss.CourseName;
}
}
Or you could add an IsSelected property to the Section and SubSection types and bind the IsSelected property of the TreeView to it:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
Then you get the selected item by iterating through the ItemsSource of the TreeView and look for the item that has the IsSelected source property set to true.

How to display values of a bound datagrid

I have a WPF application and I'm usign the MVVM design approach.
I have an ObservableObject which holds a list of the class Transaction which is below;
public Transaction
{
public string Ammount;
public string Direction;
}
The ObservableObject in my ViewModel is below
public ObservableCollection<Transactions> ListOfUserTransactions
{
get { return _localTransactionData; }
set
{
_localTransactionData = value;
RaisePropertyChanged("ListOfUserTransactions");
}
}
And my view binds in this way;
<DataGrid ItemsSource="{Binding ListOfUserTransactions}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Ammount" Binding="{Binding Path=Ammount}" Foreground="Black" IsReadOnly="True" />
<DataGridTextColumn Header="Direction" Binding="{Binding Path=Direction}" Foreground="Black" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
This does work in a way as the number of rows produced equal what i was expecting but all the rows are empty, i presume it is the way i am binding the DataGridTextColumn but im unsure as how to do it, hope there is enough detail in the above question.
EDIT
below is how the transaction list is created
ObservableCollection<Transaction> _localTransactionData = new ObservableCollection<Transaction>();
public TransactionsViewModel(User passedThroughUser)
{
_user = passedThroughUser;
_listOfTransactions = _user.Transactions;
foreach (var item in _listOfTransactions)
{
LocalTransactionsList tran = new LocalTransactionsList();
tran.Ammount = item.Ammount.ToString(); ;
tran.Direction = item.Direction;
_localTransactionData.Add(tran);
}
}
Your problem is here:
public Transaction
{
public string Ammount;
public string Direction;
}
should be:
public Transaction
{
public string Ammount {get;set;}
public string Direction {get;set;}
}
Binding properties must have the setter and getter.
You can get the behaviour you are after by changing your Transaction class to this...
public class Transaction : INotifyPropertyChanged
{
private string _amount;
public string Amount
{
[DebuggerStepThrough]
get { return _amount; }
[DebuggerStepThrough]
set
{
if (value != _amount)
{
_amount = value;
OnPropertyChanged("Amount");
}
}
}
private string _direction;
public string Direction
{
[DebuggerStepThrough]
get { return _direction; }
[DebuggerStepThrough]
set
{
if (value != _direction)
{
_direction = value;
OnPropertyChanged("Direction");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
This will enable the WPF binding engine to access your properties and bind them correctly.
More about INPC here http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx

WPF Combobox initial dictionary binding value not showing

I have a wpf combobox bound to a property LogicalP of a class SInstance. The ItemSource for the combobox is a dictionary that contains items of type LogicalP.
If I set LogicalP in SInstance to an initial state, the combobox text field shows empty. If I select the pulldown all my dictionary values are there. When I change the selection LogicalP in SInstance gets updated correctly. If I change Sinstance in C# the appropriate combobox value doesn't reflect the updated LogicalP from the pulldown.
I've set the binding mode to twoway with no luck. Any thoughts?
My Xaml:
<UserControl.Resources>
<ObjectDataProvider x:Key="PList"
ObjectType="{x:Type src:MainWindow}"
MethodName="GetLogPList"/>
</UserControl.Resources>
<DataTemplate DataType="{x:Type src:SInstance}">
<Grid>
<ComboBox ItemsSource="{Binding Source={StaticResource PList}}"
DisplayMemberPath ="Value.Name"
SelectedValuePath="Value"
SelectedValue="{Binding Path=LogicalP,Mode=TwoWay}">
</ComboBox>
</Grid>
</DataTemplate>
My C#:
public Dictionary<string, LogicalPType> LogPList { get; private set; }
public Dictionary<string, LogicalPType> GetLogPList()
{
return LogPList;
}
public class LogicalPType
{
public string Name { get; set; }
public string C { get; set; }
public string M { get; set; }
}
public class SInstance : INotifyPropertyChanged
{
private LogicalPType _LogicalP;
public string Name { get; set; }
public LogicalPType LogicalP
{
get { return _LogicalP; }
set
{
if (_LogicalP != value)
{
_LogicalP = value;
NotifyPropertyChanged("LogicalP");
}
}
}
#region INotifyPropertyChanged Members
#endregion
}
They are not looking at the same source.
You need to have SInstance supply both the LogPList and LogicalP.
_LogicalP is not connected to LogPList
If you want to different objects to compare to equal then you need to override Equals.
Here's my working solution. By moving the dictionary retrieval GetLogPList to the same class as that supplies the data (as suggested by Blam) I was able to get the binding to work both ways. I changed binding to a list rather than a dictionary to simplify the combobox
Here's the updated Xaml showing the new ItemsSource binding and removal of the SelectedValuePath:
<DataTemplate DataType="{x:Type src:SInstance}">
<Grid>
<ComboBox ItemsSource="{Binding GetLogPList}"
DisplayMemberPath ="Name"
SelectedValue="{Binding Path=LogicalP,Mode=TwoWay}">
</ComboBox>
</Grid>
</DataTemplate>
I then changed the dictionary LogPList to static so that it would be accessible to the class SInstance:
public static Dictionary<string, LogicalPType> LogPList { get; private set; }
Finally, I moved GetLogPList to the class SInstance as a property. Note again it's returning a list as opposed to a dictionary to make the Xaml a little simpler:
public class SInstance : INotifyPropertyChanged
{
public List<LogicalPType> GetLogPList
{
get { return MainWindow.LogPList.Values.ToList(); }
set { }
}
private LogicalPType _LogicalP;
public string Name { get; set; }
public LogicalPType LogicalP
{
get { return _LogicalP; }
set
{
if (_LogicalP != value)
{
_LogicalP = value;
NotifyPropertyChanged("LogicalP");
}
}
}
#region INotifyPropertyChanged Members
#endregion
}

How can i bind datagrid to some properties of a class in wpf?

Is it possible to bind a datagrid to only selective members of a class?
The way I have made the binding presently, all the class variables have been binded(one to one mapped) to datagrid's columns.
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Now i want only a few class properties(not all) to be binded to the datagrid.
Yes, Just turn off the AutoGenerateColumns and manually specify them
In MainWindow.xaml
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Hello}" Header="Hello" />
<DataGridTextColumn Binding="{Binding World}" Header="World" />
</DataGrid.Columns>
</DataGrid>
In MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new[] { new FakeViewModel() };
}
}
In FakeViewModel.cs
namespace WpfApplication4
{
class FakeViewModel
{
public FakeViewModel()
{
Hello = "Hello";
World = "World";
Then = DateTime.Now;
}
public DateTime Then { get; set; }
public string Hello { get; set; }
public string World { get; set; }
}
}
Please note the unused Property Then!

Resources