DataGridColumns DisplayIndex Binding and DataContext change - c#

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.

Related

Why are DataGrid items empty even if it is bound to an ObservableCollection that has rows in WPF?

I'm using C# WPF
I have Datagrid that is filled by an observable collection named: "ALLTASKLIST" filled with Select data from a Database
I used DataBdinging to display data from "ALLTASKLIST" in my DataGrid
but it's empty!
XAML:
<DataGrid.Columns>
<DataGridComboBoxColumn Header="CMB">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Path=ALLTASKLIST}" />
<Setter Property="DisplayMemberPath" Value="NAMES" />
<Setter Property="SelectedValuePath" Value="CODE" />
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
Code behind:
public partial class MainWindow : Window
{
NEGIN1401Entities dbms = null;
public class ThePart1 : INotifyPropertyChanged
{
private int? _CODE;
private string _NAMES;
private int? _KIND;
private int? _idd;
public int? CODE
{
get { return _CODE; }
set
{
_CODE = value;
OnPropertyChanged("CODE");
}
}
public string NAMES
{
get { return _NAMES; }
set
{
_NAMES = value;
OnPropertyChanged("NAMES");
}
}
public int? KIND
{
get { return _KIND; }
set
{
_KIND = value;
OnPropertyChanged("KIND");
}
}
public int? idd
{
get { return _idd; }
set
{
_idd = value;
OnPropertyChanged("idd");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string strCaller = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(strCaller));
}
}
//public ObservableCollection<ThePart1> ALLTASKLIST { get; set; }
public ObservableCollection<ThePart1> ALLTASKLIST = new ObservableCollection<ThePart1>();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
dbms = new NEGIN1401Entities(Publications.TheENTTConnectionString);
ALLTASKLIST = dbms.Database.SqlQuery<ThePart1>("SELECT * FROM TCOD_ANBAR").ToObservableCollection();
var IsHaveItems = ALLTASKLIST.Count;//it will be 6 Items
//MainDatagrid.ItemsSource = ALLTASKLIST;
}
}
The Data Grid's items:
enter image description here
Result:
enter image description here
The binding path needs a property, not a field.
Your style will set values for the ComboBox at the row level, and the data context will be the collection element ThePart1, not MainWindow. Accordingly, the binding path will be interpreted relative to ThePart1, which does not have the ALLTASKLIST property.
A feature of the DataGridComboBoxColumn is that the source is obtained immediately when the DataGrid is created. Therefore, the source is usually created either in App resources or in a static property.
Try this example:
public static ObservableCollection<ThePart1> ALLTASKLIST {get;}
= new ObservableCollection<ThePart1>();
ALLTASKLIST.Clear();
for(var tp1 in dbms.Database.SqlQuery<ThePart1>("SELECT * FROM TCOD_ANBAR").ToList())
ALLTASKLIST.Add(tp1);
<DataGridComboBoxColumn Header="CMB"
ItemsSource="{x:Static local:MainWindow.ALLTASKLIST}">
I don't understand your second point, can you make it more clear?
For understanding, try adding this column:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding}"/>
</DataGrid.Columns>
thanks so much but only one thing not work yet ! it's ComboBox ! Cannot Access it in DataGridTemplateColumn or Binding for CombobBox Column
Without seeing your code, I can't tell the cause of the problem.
I made a simple example to demonstrate the different ways to create and bind a ComboBox column.
Consider this example.
Perhaps you will find something useful for you and you yourself will understand what is the cause of the error.
using System;
using System.Collections.ObjectModel;
using System.Windows;
namespace Core2022.SO.DataGridComboBoxColumnExample
{
public partial class ExampleWindow : Window
{
public ExampleWindow()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
EntitiesViewModel vm = (EntitiesViewModel)DataContext;
Random random = new();
for (int i = 0; i < 10; i++)
{
int num = random.Next();
vm.Entities.Add(new IdEntity { Id = num });
vm.Ids.Add(num);
EntitiesViewModel.StaticIds.Add(num);
}
}
}
public class EntitiesViewModel
{
public ObservableCollection<int> Ids { get; } = new();
public static ObservableCollection<int> StaticIds { get; } = new();
public ObservableCollection<IdEntity> Entities { get; } = new();
}
public class IdEntity
{
public int Id { get; set; }
}
}
<Window x:Class="Core2022.SO.DataGridComboBoxColumnExample.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Core2022.SO.DataGridComboBoxColumnExample"
mc:Ignorable="d"
Title="ExampleWindow" Height="450" Width="800"
Loaded="OnLoaded">
<FrameworkElement.DataContext>
<local:EntitiesViewModel/>
</FrameworkElement.DataContext>
<Grid>
<DataGrid ItemsSource="{Binding Entities}">
<DataGrid.Columns>
<DataGridComboBoxColumn SelectedItemBinding="{Binding Id, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{x:Static local:EntitiesViewModel.StaticIds}"/>
<DataGridComboBoxColumn SelectedItemBinding="{Binding Id, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding DataContext.Ids, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding Id, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{x:Static local:EntitiesViewModel.StaticIds}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding Id, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding DataContext.Ids, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Pay attention to the second column - it is not created due to a binding error.
That's exactly what I wrote in point 3.
First of all you should bind to property, not field. Change it
public ObservableCollection<ThePart1> ALLTASKLIST = new ObservableCollection<ThePart1>();
to this
public ObservableCollection<ThePart1> ALLTASKLIST { get; set; }
I think you have problems with DataContext of DataGrid. Try to set it to DataGrid directly within XAML by creating a local instance of MainWindow within XAML.(See EldHasp's question)
Also, try to set UpdateSourceTrigger property to PropertyChanged because maybe columns are updated but you can't simply see updates without focus. Set DataGrid's AutoGenerateColumns property to false, so it will not create new columns.

How to set ComboBoxItem property?

I am trying to hide an item in the Combobox when it has been selected and this is how my code looks like right now:
VeiwModel.cs
public class SortList
{
public string Key { get; set; }
public string Value { get; set; }
public bool IsSelectable { get; set; }
}
private void InitSortList()
{
ObservableCollection<SortList> sl = new ObservableCollection<SortList>();
foreach(var i in defaultSortList)
{
SortList s = new SortList();
s.Key = i.Key.ToString();
s.Value = i.Value.ToString();
s.IsSelectable = false;
sl.Add(s);
}
_items = sl;
}
private ObservableCollection<SortList> _items = new ObservableCollection<SortList>();
public ObservableCollection<SortList> Items
{
get {
return _items; }
}
private SortList _selectedSort;
public SortList SelectedItem
{
get { return _selectedSort; }
set
{
if(_selectedSort != value)
{
_selectedSort = value;
_selectedSort.IsSelectable = false;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
}
MainPage.xaml
<ComboBox Header="Sort 1" HorizontalAlignment="Stretch"
Name="Sort_1" SelectionChanged="comboSelectionChanged"
ItemsSource="{Binding Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectedValuePath="Key"
DisplayMemberPath="Value"
>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem" BasedOn="ComboBoxIem">
<Setter
Property="IsEnabled"
Value="{Binding Items.IsSelectable, Mode=TwoWay}" />
//Binding IsSelectable doesnt work either
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
I am not sure how the Binding part works on the Setter property as I think it's not getting the IsSelectable property from the Items class....
Please refer to document here, UWP does not support bindings in Style Setters. It will not effect when you binding ItemContainerStyle style.
Windows Presentation Foundation (WPF) and Microsoft Silverlight supported the ability to use a Binding expression to supply the Value for a Setter in a Style. The Windows Runtime doesn't support a Binding usage for Setter.Value (the Binding won't evaluate and the Setter has no effect, you won't get errors, but you won't get the desired result either). When you convert XAML styles from Windows Presentation Foundation (WPF) or Microsoft Silverlight XAML, replace any Binding expression usages with strings or objects that set values, or refactor the values as shared {StaticResource} markup extension values rather than Binding -obtained values.
For this scenario, A workaround could be a helper class with attached properties for the source paths of the bindings. It will create the binding expression in code behind in a PropertyChangedCallback of the helper property.
I have edited your code and xaml, please refer to the following code the implement.
<Page.DataContext>
<local:ViewModel />
</Page.DataContext>
<Grid>
<ComboBox
Name="Sort_1"
HorizontalAlignment="Stretch"
DisplayMemberPath="Value"
Header="Sort 1"
ItemsSource="{Binding Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectedValuePath="Key"
SelectionChanged="comboSelectionChanged">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="local:BindingHelper.IsEnable" Value="IsSelectable" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</Grid>
C# Code
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
private void comboSelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
public class BindingHelper
{
public static string GetIsEnable(DependencyObject obj)
{
return (string)obj.GetValue(IsEnableProperty);
}
public static void SetIsEnable(DependencyObject obj, string value)
{
obj.SetValue(IsEnableProperty, value);
}
// Using a DependencyProperty as the backing store for IsEnable. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsEnableProperty =
DependencyProperty.RegisterAttached("IsEnable", typeof(string), typeof(BindingHelper), new PropertyMetadata(null, GridBindingPathPropertyChanged));
private static void GridBindingPathPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var propertyPath = e.NewValue as string;
if (propertyPath != null)
{
var bindingProperty =
e.Property == IsEnableProperty
? ComboBoxItem.IsEnabledProperty
: null;
BindingOperations.SetBinding(
obj,
bindingProperty,
new Binding { Path = new PropertyPath(propertyPath) });
}
}
}
public class ViewModel : INotifyPropertyChanged
{
public class SortList : INotifyPropertyChanged
{
public string Key { get; set; }
public string Value { get; set; }
private bool _isSelectable;
public bool IsSelectable
{
get { return _isSelectable; }
set
{
_isSelectable = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
public ViewModel()
{
defaultSortList = new Dictionary<string, string>();
defaultSortList.Add("0", "item");
defaultSortList.Add("1", "item1");
defaultSortList.Add("2", "item2");
defaultSortList.Add("3", "item3");
InitSortList();
}
private Dictionary<string, string> defaultSortList;
private void InitSortList()
{
ObservableCollection<SortList> sl = new ObservableCollection<SortList>();
foreach (var i in defaultSortList)
{
SortList s = new SortList();
s.Key = i.Key.ToString();
s.Value = i.Value.ToString();
s.IsSelectable = true;
sl.Add(s);
}
_items = sl;
}
private ObservableCollection<SortList> _items = new ObservableCollection<SortList>();
public ObservableCollection<SortList> Items
{
get
{
return _items;
}
}
private SortList _selectedSort;
public event PropertyChangedEventHandler PropertyChanged;
public SortList SelectedItem
{
get { return _selectedSort; }
set
{
if (_selectedSort != value)
{
_selectedSort = value;
_selectedSort.IsSelectable = false;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
}
}

WPF DataGrid calling command, binding

I have just started learning WPF yesterday and my goal is to create window with simple grid with hotel booking information. For now there are just room number, number of guests, dates and "Action" columns. In the "Actions" column there is "Save" button. It should be able to save updates or create new booking when clicked in new row. The problem is when I click "Save" button SaveBooking method is not invoked. I'm also not sure how to properly bind to CurrentBooking object. As I am new to WPF I tried to figure it out from few tutorials. Here's what I've created.
XAML:
<Window x:Class="HotelApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HotelApp"
mc:Ignorable="d"
Title="MainWindow" Height="800" Width="1000">
<Grid>
<TabControl>
<TabItem Header="Bookings">
<DataGrid AutoGenerateColumns = "False" ItemsSource="{Binding Bookings}">
<DataGrid.Columns>
<DataGridTextColumn Header = "Room" Binding = "{Binding Room, Mode=TwoWay}" />
<DataGridTextColumn Header = "Floor" Binding = "{Binding NumOfGuests, Mode=TwoWay}" />
<DataGridTextColumn Header = "From" Binding = "{Binding From, Mode=TwoWay}"/>
<DataGridTextColumn Header = "To" Binding = "{Binding To, Mode=TwoWay}"/>
<DataGridTemplateColumn Header = "Actions">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Save" Command="{Binding DataContext.SaveBookingCommand }" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Guests" />
</TabControl>
</Grid>
</Window>
MODEL:
public class BookingModel : ObservableObject
{
private int _room;
public int Room
{
get => _room;
set
{
if (value != _room)
{
_room = value;
OnPropertyChanged("Room");
}
}
}
private int _numOfGuests;
public int NumOfGuests
{
get => _numOfGuests;
set
{
_numOfGuests = value;
OnPropertyChanged("NumOfGuests");
}
}
private DateTime _from;
public DateTime From
{
get => _from;
set
{
_from = value;
OnPropertyChanged("From");
}
}
private DateTime _to;
public DateTime To
{
get => _to;
set
{
_to = value;
OnPropertyChanged("To");
}
}
}
VIEWMODEL:
public class MainWindowVM : ObservableObject
{
private readonly IBookingService _bookingService;
private ICommand _saveBookingCommand;
public ICommand SaveBookingCommand
{
get
{
if (_saveBookingCommand == null)
{
_saveBookingCommand = new RelayCommand(
param => SaveBooking(),
param => (CurrentBooking != null)
);
}
return _saveBookingCommand;
}
}
private ObservableCollection<BookingModel> _Bookings { get; set; }
private BookingModel _currentBookng;
public BookingModel CurrentBooking
{
get { return _currentBookng; }
set
{
if (value != _currentBookng)
{
_currentBookng = value;
OnPropertyChanged("CurrentBooking");
}
}
}
public ObservableCollection<BookingModel> Bookings
{
get { return _Bookings; }
set { _Bookings = value; }
}
public MainWindowVM(IBookingService bookingService)
{
_bookingService = bookingService;
BrowseBookings();
}
public void BrowseBookings()
{
var bookings = _bookingService.Browse().Select(x => new BookingModel { Room = x.Room.RoomId, NumOfGuests = x.NumOfGuests, From = x.From, To = x.To });
Bookings = new ObservableCollection<BookingModel>(bookings);
}
private void SaveBooking()
{
// send CurrentBooking to service
}
}
RelayCommand:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameters)
{
return _canExecute == null ? true : _canExecute(parameters);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameters)
{
_execute(parameters);
}
#endregion // ICommand Members
}
Your command is in the datacontext of the entire datagrid MainWindowVM.
Your button's datacontext is that of the row - a BookingModel.
You need some relativesource on that binding.
In principle that looks like this:
{Binding DataContext.ParentVMProperty,
RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
And your type, in this case, will be DataGrid.
You can also bind selecteditem on the datagrid and when they click the button ensure that is selected using the datagrid properties for selection.
Or
You can have a commandparameter on the command which is
CommandParameter="{Binding .}"
Relaycommand usually comes in two flavours one being RelayCommand
Maybe I missed it but I don't see that in your implementation. I'd suggest you go grab the source code for MVVM Light and paste into your solution for a more complete implementation. Or just add the nuget package if you're not using .net core. You want the commandwpf namespace version of relaycommand.
You left out a lot of code, so I don't know which nuget package you used for your ObservableObject. Anywho, I faked the ObservableObject and got the binding working. The main problem was that you were trying to bind SaveBookingCommand at the BookingModel level, when in your code you have it written in the MainWindowVM level.
You can easily fix this by parenting your MainWindowVM in your BookingModel, and change your binding to be Command={Binding Parent.SaveBookingCommand}.
Here's some pointers to the edits that I made:
MainWindow.xaml.cs:
<DataTemplate>
<Button Content="Save" Command="{Binding Parent.SaveBookingCommand}" />
</DataTemplate>
BookingModel.cs:
public class BookingModel : ObservableObject
{
public MainWindowVM Parent { get; private set; }
public BookingModel()
{
this.Parent = null;
}
public BookingModel(MainWindowVM parent)
{
this.Parent = parent;
}
// ... you know the rest
MainWindowVM.cs:
public MainWindowVM : ObservableObject
{
public void BrowseBookings()
{
// NOTICE that I added 'this' as the parameter argument to connect MainWindowVM to the BookingModel.
var bookings = _bookingService.Browse().Select(x => new BookingModel(this) { Room = x.Room, NumOfGuests = x.NumOfGuests, From = x.From, To = x.To });
Bookings = new ObservableCollection<BookingModel>(bookings);
CurrentBooking = Bookings.First();
}
// ... you know the rest

WPF - New item row not being shown in DataGrid when not binding to the ItemsSource property

I'm trying to create a custom DataGrid based on the WPF's DataGrid control. I created another property called "ItemsDataSource" and use it to bind collections from my ViewModels. When this property raises the ValueChanged event, it sets the value of ItemsSource to the value of ItemsDataSource.
This works fine when the grid is in Read Only mode, but when I set the property CanUserAddRows to True, if the ItemsDataSource is empty, my DataGrid never shows the new line to add new rows. BUT, if I change the binding back to ItemsSource instead of my ItemsDataSource, the DataGrid shows the new line.
Here is the partial code for my custom Grid:
public partial class NewDataGrid : DataGrid
{
public NewDataGrid()
{
InitializeComponent();
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsDataSourceProperty, typeof(NewDataGrid));
dpd?.AddValueChanged(this, (s, a) =>
{
ItemsSource = ItemsDataSource.Cast<object>().ToList();
});
}
public IList ItemsDataSource
{
get { return (IList)GetValue(ItemsDataSourceProperty); }
set { SetValue(ItemsDataSourceProperty, value); }
}
public static readonly DependencyProperty ItemsDataSourceProperty =
DependencyProperty.Register("ItemsDataSource", typeof(IList), typeof(NewDataGrid), new PropertyMetadata(null));
}
And here is how I'm doing the binding in XAML:
<WPF:NewDataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
ItemsDataSource="{Binding Path=DataContext.DataWrapperList, RelativeSource={RelativeSource AncestorType={x:Type Grid}}}"
SelectedValue="{Binding Path=DataContext.SelectedDataWrapper, RelativeSource={RelativeSource AncestorType={x:Type Grid}}}"
AutoGenerateColumns="False"
Validation.ErrorTemplate="{x:Null}"
Margin="0"
VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
CanUserAddRows="True"
CanUserDeleteRows="True"
IsReadOnly="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" Width="*" SortMemberPath="Name" />
<DataGridTextColumn Header="Quantity" Binding="{Binding Path=Quantity}" Width="*" SortMemberPath="Quantity" />
</WPF:PAENewDataGrid>
Here is how the DataListWrapper property is declared in my DataContext:
public ObservableCollection<DataWrapper> DataWrapperList;
Here is my DataWrapper class:
public class DataWrapper : BaseWrapper
{
private DataWrapperDTO _data;
public DataWrapper()
{
_data = new DataWrapperDTO();
}
public DataWrapper(DataWrapperDTO data)
{
_data = data;
}
public string Name
{
get { return _data.Name; }
set
{
_data.Name = value;
RaisePropertyChanged(nameof(Name));
}
}
public int Quantity
{
get { return _data.Quantity; }
set
{
_data.Quantity = value;
RaisePropertyChanged(nameof(Quantity));
}
}
}
Does anyone know how to force the DataGrid to always show this new line whenever the CanUserAddRows property is set to True?
Please try not to replace the ItemsSource every time a new item is added:
public NewDataGrid()
{
InitializeComponent();
ItemsSource = new ObservableCollection<object>();
var dpd = DependencyPropertyDescriptor.FromProperty(ItemsDataSourceProperty, typeof(NewDataGrid));
dpd?.AddValueChanged(this, (s, a) =>
{
ItemsSource.Clear();
ItemsSource.Add(ItemsDataSource.Cast<object>().ToList());
});
}
After struggling a little while with this problem, it seems that I've found a solution. Registering my DependencyProperty with a PropertyChangedCallback and assigning the ItemsSource to the new value inside this callback makes the grid add the blank line to add new values.
The NewDataGrid class code looks like this now:
public partial class NewDataGrid : DataGrid
{
public NewDataGrid()
{
InitializeComponent();
//Removed the DependencyPropertyDescriptor
}
public IList ItemsDataSource
{
get { return (IList)GetValue(ItemsDataSourceProperty); }
set { SetValue(ItemsDataSourceProperty, value); }
}
public static readonly DependencyProperty ItemsDataSourceProperty =
DependencyProperty.Register("ItemsDataSource", typeof(IList), typeof(NewDataGrid), new PropertyMetadata(null, ItemsDataSourceChanged));
private static void ItemsDataSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var grid = sender as NewDataGrid;
if (grid == null) return;
grid.ItemsSource = ((IList)args.NewValue).Cast<object>().ToList();
}
}
Thanks for the help guys!

NumericUpDown in GridCell - WPF

I have a grid with a collection of "ItemPresupusto". I need to add a NumericUpDown (by mahApps) to be able to modify the "Cantidad" property of each "ItemPresupuesto" and every time I modify that property, I need to update data in the UI. I've tried everything, but I can not do it. Im use MVVM Light Some help. Thank you!
.XAML
<DataGrid IsReadOnly="True"
SelectionUnit="FullRow"
AutoGenerateColumns="False"
GridLinesVisibility="Horizontal"
ItemsSource="{Binding Articulos}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Cantidad" MinWidth="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<mahApps:NumericUpDown Minimum="1"
IsTabStop="False"
Value="{Binding Cantidad, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
ViewModel
public class PresupuestosViewModel : ViewModelBase
{
public IEnumerable<ItemPresupuesto> Articulos => new ObservableCollection<ItemPresupuesto>(Presupuesto.Items);
}
Class
public class ItemPresupuesto: EntidadBase
{
public decimal Cantidad { get; set; }
}
public class Presupuesto : EntidadBase
{
public virtual List<ItemPresupuesto> Items { get; }
}
The ItemPresupuesto class should implement the INotifyPropertyChanged and interface and raise change notifications for the source property that is bound to the control that you want to refresh whenever the Cantidad or Prico properties are set:
public class ItemPresupuesto : INotifyPropertyChanged
{
private decimal _cantidad;
public decimal Cantidad
{
get { return _cantidad; }
set { _cantidad = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(Total)); }
}
private decimal _prico = 1;
public decimal Prico
{
get { return _prico; }
set { _prico = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(Total)); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public decimal Total => _prico * _cantidad;
}

Categories

Resources