I'm using MVVM. I bind datagrid to collection with some code:
<commonMVVMControls:GridControl DataContext="{Binding Path=ClientsListGrid,
Mode=TwoWay}"
It's DataGridControl class:
public class GridControl : DataGrid
{
public GridControl()
{
this.DataContextChanged += new System.Windows.DependencyPropertyChangedEventHandler(GridControl_DataContextChanged);
}
void GridControl_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
var bindingItemsSource = new Binding("ItemsSource");
bindingItemsSource.Source = this.DataContext;
this.SetBinding(DataGrid.ItemsSourceProperty, bindingItemsSource);
this.RowStyle = new Style(typeof(DataGridRow));
this.RowStyle.Setters.Add(new Setter(DataGridRow.IsSelectedProperty, new Binding("IsSelected")));
}
Now a snippet of code in ViewModel:
var selectedClient = this.ClientsListGrid.ItemsSource.Where(x => x.IsSelected);
if (!selectedClient.Any())
{
MessageBox.Show(Resource.Resource.UpdateUserError, Resource.Resource.Warning, MessageBoxButton.OK, MessageBoxImage.Stop,
MessageBoxResult.OK);
return;
}
var viewModel = new AddOrUpdateClientViewModel(_serviceContext, selectedClient.First());
It works well. But if I scroll datagrid down or up, it stops working and IsSelected always equal false.
Try and see what happens if you disable virtualization. It might have something to do with that.
Related
I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.
I am trying to create custom columns with a label and a combobox for the column headers.
The first time the grid is loaded properly. On the click of the button, I change the itmesources of the Datagrid. I see the cells of the datagrid getting updated, but not the headers. I also see that the Datagrid_Loaded is not being called. On debugging I found that for "Binding" object I supply the binding path, but for header I supply the source, hence the header don't get updated. How to supply path for the header so that its is similar to that of Binding?
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
DataGrid grid = sender as DataGrid;
if (grid.Items.Count == 0)
{
return;
}
StageVM data = grid.Items[0] as StageVM;
if (data == null)
{
return;
}
int index = 0;
foreach (StageItemVM param in data.Stage)
{
var binding = new Binding(string.Format("Stage[{0}].StageItem.Value", index));
grid.Columns.Add(new CustomBoundColumn()
{
CanUserSort = false,
HeaderTemplateSelector = this.FindResource("ColumnHeaderTemplateSelector") as DataTemplateSelector,
HeaderStyle = this.FindResource("StageHeaderStyle") as Style,
Header = (new Binding().Source = param.StageItem.Key),
Binding = binding,
TemplateSelectorName = "StageRangeColumnTemplateSelector"
});
index++;
}
}
And the customBoundColumn class is as follows:
public class CustomBoundColumn : DataGridBoundColumn
{
public string TemplateSelectorName { get; set; }
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var binding = new Binding(((Binding)Binding).Path.Path);
binding.Source = dataItem;
var content = new ContentControl();
content.ContentTemplateSelector =(DataTemplateSelector)cell.FindResource(TemplateSelectorName);
content.SetBinding(ContentControl.ContentProperty, binding);
return content;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return GenerateElement(cell, dataItem);
}
}
Check if this can help you:
WPF DataGridTextColumn header binding
I had the same problem about binding datagrid headers with my ViewModel. It's a xaml based solution.
Having an issue with selection of my treeview.
The idea is that the user clicks the item and data is added under the node that they selected
public MainWindow()
{
InitializeComponent();
this.UserControl1.TreeView1.SelectedItemChanged += new RoutedPropertyChangedEventHandler<Object>(InterfaceTreeViewComputers_SelectionChange);
}
void InterfaceTreeViewComputers_SelectionChange(Object sender, RoutedPropertyChangedEventArgs<object> e)
{
var MyTreeView = MainWindow.UserControl1.Treeview1.Items.SourceCollection;
var TheSource = sender as TreeView;
var TheProperty = e.Source;
var ThePropertyAsTreeView = TheProperty as TreeView
TreeViewItem item = e.OriginalSource as TreeViewItem; //Equals Null
var Attempt2 = ThePropertyAsTreeView.SelectedItem //Equals Null
var Attempt3 = TheSource.SelectedItem as TreeViewItem //Equals Null
var Attempt4 = TheSource.SelectedItem //Equals onbject(String)
}
It seems that the selected item is a textblock and i cant seem to find a way to get it as a treeview item to add nodes under it.
Yes i am pretty new to this type of programming.
Thank you for any help you may provide.
Try this to get selected item:
TreeViewItem item = e.NewValue as TreeViewItem;
Or this to get previously selected item:
TreeViewItem item = e.OldValue as TreeViewItem;
e.Source and e.OriginalSource refer to the TreeView not the TreeViewItem selected. You can use breakpoint then see those properties value and type in Visual Studio's watch. That what I was doing before posting this.
I believe you are approaching this the wrong way. Most of your problems will disappear if you use DataBinding to populate your TreeView. Take into account the following class
namespace StackOverflow._20716616
{
public class Model
{
public Model()
{ Items = new ObservableCollection<Model>(); }
public Model(string text)
: this()
{ Text = text; }
public string Text { get; set; }
public ObservableCollection<Model> Items { get; set; }
}
}
Using this class as the model of an MVVM pattern, I can create a hierarchical object graph which I can bind to the TreeView using ItemsSource and a HierarchicalDataTemplate like
<TreeView ItemsSource="{Binding Path=Items}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type this:Model}" ItemsSource="{Binding Path=Items}">
<TextBlock Text="{Binding Path=Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The DataContext of the window was set up on the root node of the xaml
DataContext="{Binding RelativeSource={RelativeSource Self}}"
In the code behind, I populate the root items of the TreeView
public MainWindow()
{
Items = new ObservableCollection<Model>() { new Model("one"), new Model("two"), new Model("three") };
InitializeComponent();
}
public ObservableCollection<Model> Items { get; set; }
Then, in the SelectedItemChanged, I can just add the children to the object graph
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var model = e.NewValue as Model;
if (!model.Items.Any())
{
new List<Model>() { new Model("one"), new Model("two"), new Model("three") }
.ForEach(model.Items.Add);
}
// expand the selected item.
var item = ((TreeView)sender).ItemContainerGenerator.ContainerFromItem(model) as TreeViewItem;
item.IsExpanded = true;
}
Note, this is poor man's MVVM where I have used the Windows code behind as the ViewModel. I have also left out null checks and exception catching for the sake of brevity.
I hope this helps
Your right, i am so sorry. http://www.dotnetperls.com/ have given me heaps of insite on this language.
Im too used to programming in things like Visual Basic and Delphi. ive realize now that my MainWindow needs to perform all the pysical actions to my front end user interface and the classes in the other namespaces do not do this.
I have realize that you call a class to perform a specific operation. the class then either returns the information or stores the information in a public property. in which the MainWindow can utilize this and perform the Action required. This i didnt realizeand was trying to manipulate my User Interface from other classes.
I have since restructed my project and the information that was provided here has helped me with my events.
now i have UI coded in XAML
Main Window (Performin events and updating the UI)
Other Classes (Getting data, or designing controls) so the Main Window can add the control or the data to the UI.
I reel like a nub now.
I am using .NET framework 4.0 to build my application.
I have a combobox in which I want to turn off suggest-append mode of combobox. Instead I want suggest-only mode.
In many questions users ask for turning autoComplete feature off and everywhere I got the same answer. i.e. set IsTextSearchEnabled to False.
When IsTextSearchEnabled = True
When IsTextSearchEnabled = False
What I want is :
When User Presses Enter on the Combobox I want the Item to be appended to the textbox of the combobox.
Is this thing possible in WPF?
Like promised here is the demo. As you can see I did what I explained in my comments. I listened to text changed event.
Check it out:
<Grid>
<local:MyComboBox x:Name="comboBox" IsEditable="True"
VerticalAlignment="Center"
IsTextSearchEnabled="True">
<ComboBoxItem>hello</ComboBoxItem>
<ComboBoxItem>world</ComboBoxItem>
<ComboBoxItem>123</ComboBoxItem>
</local:MyComboBox>
</Grid>
public class MyComboBox : ComboBox
{
private string myValue;
private bool needsUpdate;
public override void OnApplyTemplate()
{
TextBox tbx = this.GetTemplateChild("PART_EditableTextBox") as TextBox;
tbx.PreviewKeyDown += (o, e) =>
{
this.needsUpdate = true;
};
tbx.TextChanged += (o, e) =>
{
if (needsUpdate)
{
myValue = tbx.Text;
this.needsUpdate = false;
}
else
{
tbx.Text = myValue;
}
};
base.OnApplyTemplate();
}
}
I am just new to WPF and I am having problems displaying my record. It seems that my records are "shy" when it comes to displaying them, even though I have all my records already.
Code for my App.xaml.cs:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Window1 window = new Window1();
var viewModel = new Window1ViewModel();
window.PHGrid.ItemsSource = viewModel.ViewModels;
window.Show();
}
Code for Window1ViewModel:
public class Window1ViewModel : ViewModelBase
{
private readonly DAPHContrib _contribRepository;
private ObservableCollection<ViewModelBase> _viewModelBases;
public ObservableCollection<ViewModelBase> ViewModels
{
get
{
if (_viewModelBases == null)
{
_viewModelBases = new ObservableCollection<ViewModelBase>();
}
return _viewModelBases;
}
}
public Window1ViewModel()
{
_contribRepository = new DAPHContrib();
//Create instance of our view model to add it in our collection
PHContribViewModel viewModel = new PHContribViewModel(_contribRepository);
ViewModels.Add(viewModel);
}
}
Here's my Window1.xaml UPDATED:
<Window x:Class="Wabby_App.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Wabby_App.ViewModels"
xmlns:v="clr-namespace:Wabby_App.Views"
Title="Utos ng mahal ko"
Height="300"
Width="300">
<Grid>
<DataGrid
AutoGenerateColumns="True"
Height="200"
HorizontalAlignment="Center"
Name="PHGrid"
VerticalAlignment="Center"
Width="200"
ItemsSource="{Binding ViewModels}"/>
</Grid>
Output:
Hope you can help me with this.
based on your comments you have view models collection (ObservableCollection<ViewModelBase>)
and inside each of these view model base instances (PHContribViewModel) you have another collection ObservableCollection<PHContrib_Entity>.
Hence you have two levels of nested collections and one datagrid to map. This wont work as it is. For this you would need to flatten this 2 level hierarchy of collections into one list of type ObservableCollection<PHContrib_Entity>.
Use LINQ to do that...
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
Window1 window = new Window1();
var viewModel = new Window1ViewModel();
window.PHGrid.ItemsSource
= viewModel.ViewModels.SelectMany(vm => vm.PHContribEntities).ToList();
window.Show();
}
Let me know if this helps...
Your View (Window1) is not binding to ViewModel, it's just setting the control's ItemsSource to a property of the ViewModel which is an incorrect way to implement MVVM. What you need to do is set DataContext of Window1 to instance of ViewModel (Bind View to ViewModel). So, you need to update your code in the OnStartup method.
from
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Window1 window = new Window1();
var viewModel = new Window1ViewModel();
window.PHGrid.ItemsSource = viewModel.ViewModels;
window.Show();
}
to
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Window1 window = new Window1();
var viewModel = new Window1ViewModel();
window.DataContext = viewModel;
window.Show();
}
Update
You also need to set ItemsSource property of datagrid to property in ViewModel
<DataGrid ItemsSource={Binding ViewModels} ..