I am trying to make a list box with grouping on WPF. This can be easily done as described in WPF4 Unleashed and any other tutorial on the Web.
XAML (here are two lists with and without grouping + button to update their common item source):
<Window x:Class="GroupTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Path=Items}" x:Name="_listBox1">
<ListBox.GroupStyle>
<x:Static Member="GroupStyle.Default" />
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Grid.Row="1" ItemsSource="{Binding Path=Items}" x:Name="_listBox2">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="2" Content="Update Items" Click="Button_Click" Focusable="False"/>
</Grid>
</Window>
Code (here I set grouping when page loaded and update/replace item source when the button is clicked):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
namespace GroupTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public class Item
{
public string Name { get; set; }
public bool Flag { get; set; }
}
private List<Item> _items;
public List<Item> Items
{
get { return _items; }
set
{
if (_items != value)
{
_items = value;
NotifyPropertyChanged("Items");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public MainWindow()
{
InitializeComponent();
MakeItems();
DataContext = this;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ICollectionView view = CollectionViewSource.GetDefaultView(_listBox1.Items);
view.GroupDescriptions.Add(new PropertyGroupDescription("Flag"));
}
private void MakeItems()
{
_items = new List<Item>();
_items.Add(new Item() { Name = "1", Flag = true });
_items.Add(new Item() { Name = "2", Flag = true });
_items.Add(new Item() { Name = "3", Flag = false });
_items.Add(new Item() { Name = "4", Flag = true });
_items.Add(new Item() { Name = "5", Flag = false });
}
private void UpdateItems()
{
Items = new List<Item>(_items);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
UpdateItems();
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
It works, but there is a strange bug that I cannot work around. The list #1 with grouping loses focus every time I update its item source. While the list #2 without grouping keeps the focus.
Here is a link to the complete project source: https://dl.dropbox.com/u/60611528/GroupTest.zip
Any suggestions? Thanks in advance!
Update
I tried making Items an ObservableCollection<>, but this did not help. The focus still disappears from the grouped list.
Update 2
I my real app I have Items in a model class, which does not know about the list boxes. I hope for a solution that allows to fix the problem without tight coupling the window and model classes.
Here's a solution to the problem.
I've modified it to make it clear the data is changing. There's probably a simpler solution.
The idea is to detect if the Grouped ListBox has the focus on any of its items...if so, then the focus will be restored back to the listbox....because that's what is being lost.
I imagine it has something to do with the currency and ListCollectionView when in grouping mode, or it could be something to do with UI Virtualization...which is disabled when you using Grouping on a ListBox (because the Style sets ScrollViewer.CanContentScroll=False)
(you could look into the Reference Source code to dig a bit deeper on the behaviour).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace GroupTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public class Item
{
public string Name { get; set; }
public bool Flag { get; set; }
}
public ObservableCollection<Item> Items { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public MainWindow()
{
Items = new ObservableCollection<Item>();
InitializeComponent();
MakeItems();
DataContext = this;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ICollectionView view = CollectionViewSource.GetDefaultView(_listBox1.Items);
view.GroupDescriptions.Add(new PropertyGroupDescription("Flag"));
}
bool bFlip = false;
private void MakeItems()
{
Items.Clear();
if (bFlip)
{
Items.Add(new Item() { Name = "1", Flag = true });
Items.Add(new Item() { Name = "2", Flag = true });
Items.Add(new Item() { Name = "3", Flag = false });
Items.Add(new Item() { Name = "4", Flag = true });
Items.Add(new Item() { Name = "5", Flag = false });
bFlip = false;
}
else
{
Items.Add(new Item() { Name = "1", Flag = true });
Items.Add(new Item() { Name = "2", Flag = true });
Items.Add(new Item() { Name = "3", Flag = false });
Items.Add(new Item() { Name = "4", Flag = true });
Items.Add(new Item() { Name = "5", Flag = false });
Items.Add(new Item() { Name = "A", Flag = false });
Items.Add(new Item() { Name = "B", Flag = false });
bFlip = true;
}
}
private void UpdateItems()
{
bool bListBox1HadFocus = false;
IInputElement focussedelement = Keyboard.FocusedElement;
ListBoxItem lbifocussed = focussedelement as ListBoxItem;
Item itemwithfocus = (lbifocussed != null ? lbifocussed.Content as Item : null);
for (int i = 0; i < Items.Count; i++)
{
ListBoxItem lbi = _listBox1.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
if (lbi == lbifocussed)
{
bListBox1HadFocus = true;
break;
}
}
Item oldselecteditem1 = _listBox1.SelectedItem as Item;
Item oldselecteditem2 = _listBox2.SelectedItem as Item;
MakeItems();
// Set back the selections to what they were
foreach (Item item in Items)
{
if (oldselecteditem1 != null && item.Name == oldselecteditem1.Name)
{
_listBox1.SelectedItem = item;
}
if (oldselecteditem2 != null && item.Name == oldselecteditem2.Name)
{
_listBox2.SelectedItem = item;
}
}
if (bListBox1HadFocus)
{
_listBox1.Focus();
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
UpdateItems();
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
If you want a version that is closer to your original then:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace GroupTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public class Item
{
public string Name { get; set; }
public bool Flag { get; set; }
}
private List<Item> _items;
public List<Item> Items
{
get { return _items; }
set
{
if (_items != value)
{
_items = value;
NotifyPropertyChanged("Items");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public MainWindow()
{
InitializeComponent();
MakeItems();
DataContext = this;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ICollectionView view = CollectionViewSource.GetDefaultView(_listBox1.Items);
view.GroupDescriptions.Add(new PropertyGroupDescription("Flag"));
}
private void MakeItems()
{
_items = new List<Item>();
_items.Add(new Item() { Name = "1", Flag = true });
_items.Add(new Item() { Name = "2", Flag = true });
_items.Add(new Item() { Name = "3", Flag = false });
_items.Add(new Item() { Name = "4", Flag = true });
_items.Add(new Item() { Name = "5", Flag = false });
}
private void UpdateItems()
{
bool bListBox1HadFocus = false;
IInputElement focussedelement = Keyboard.FocusedElement;
ListBoxItem lbifocussed = focussedelement as ListBoxItem;
Item itemwithfocus = (lbifocussed != null ? lbifocussed.Content as Item : null);
bool blistbox1hasfocus = (lbifocussed != null ? lbifocussed.IsFocused : false);
for (int i = 0; i < Items.Count; i++)
{
ListBoxItem lbi = _listBox1.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
if (lbi == lbifocussed)
{
bListBox1HadFocus = true;
break;
}
}
Items = new List<Item>(_items);
if (bListBox1HadFocus)
{
_listBox1.Focus();
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
UpdateItems();
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Related
I had a Model class named as FormModel.cs while coding WPF using C#..
Below is my code. I am clueless that even though debugger is going into setter method of Name, PropertyChanged flag is not raising.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace MVVM.Models
{
public class FormModel : INotifyPropertyChanged
{
private string _name;
public FormModel()
{
_name = "";
_bColor = "";
_fColor = "";
}
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
private string _bColor;
public string BColor
{
get { return _bColor; }
set { _bColor = "RED"; }
}
private string _fColor;
public string FColor
{
get { return _fColor; }
set { _fColor = "BLUE"; }
}
public void apply(string Name, string BColor, string FColor)
{
this.Name = Name;
this.BColor = BColor;
this.FColor = FColor;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if(this.PropertyChanged!=null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
if(propName=="Name")
{
MessageBox.Show("Hi" + Name);
}
}
}
}
}
Here are remaining Files I edited in question in quest on request
MainViewModel.cs
using MVVM.Models;
using MVVM.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows;
namespace MVVM.ViewModels
{
public class MainViewModel : ViewModelBase
{
private FormModel FModel;
private ICommand refresh;
public ICommand Refresh
{
get { return refresh; }
set { refresh = value; }
}
public MainViewModel()
{
FModel = new FormModel();
string s = "Pratik";
object o=(object)s;
refresh = new RelayCommand(new Action<object>(getGreet));
}
public void getGreet(object s1)
{
FModel.apply("dsf", "sf", "sf");
MessageBox.Show(s1.ToString());
}
private string _name;
public string Name
{
get { return _name; }
set {
_name = value;
FModel.Name = value;
}
}
}
class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_action(parameter);
}
}
}
ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace MVVM.ViewModels
{
public abstract class ViewModelBase: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler!=null)
{
if (propertyName == "Name")
{
//Command
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
}
Below are View Files
MainView.xaml
<Window x:Class="MVVM.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:MVVM.ViewModels"
Title="UI" Height="300" Width="300">
<Grid Background="RED">
<TextBox Name="TT" TextWrapping="Wrap" Text="{Binding Name}" Margin="20,15,160,225" />
<TextBox TextWrapping="Wrap" Text="Back Color" Margin="20,73,195,167" />
<TextBox TextWrapping="Wrap" Text="Font Color" Margin="20,122,195,118"/>
<Label Content="getGreet" HorizontalAlignment="Left" Margin="187,90,0,0" VerticalAlignment="Top"/>
<Button Content="Finish" Command="{Binding Refresh}" HorizontalAlignment="Left" Margin="112,163,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
MainView.xaml.cs
//using MVVM.Models;
using MVVM.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace MVVM.Views
{
/// <summary>
/// Interaction logic for UI.xaml
/// </summary>
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
And Main Files...
App.xaml
<Application x:Class="MVVM.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="OnStartup">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.cs
using MVVM.ViewModels;
using MVVM.Views;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
namespace MVVM
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public void OnStartup(object sender, StartupEventArgs e)
{
// Create the ViewModel and expose it using the View's DataContext
MainView view = new MainView();
view.DataContext = new MainViewModel();
view.Show();
}
}
}
Please help.
You are binding to the Name property of your MainViewModel not the Name property of your Model. Even thought this is essentially a passthrough to the Model.Name property, the binding is attaching to the PropertyChanged event of the MainViewModel which doesn't get raised in your setter.
Add an OnPropertyChanged call in the setter of your MainViewModel.Name property and it should work.
public string Name
{
get { return _name; }
set
{
_name = value;
FModel.Name = value;
OnPropertyChanged("Name");
}
}
I have a grid in WPF, auto-generating columns. How can I dynamically hide columns using data annotations?
I thought of having a property in my model to specify whether the column is visible, but I'm not sure how to do it.
My model, bound to the grid:
public class Template
{
public string County { get; set; }
public string Operator { get; set; }
public string Field { get; set; }
}
Here is a sample which uses attributes to hide columns. It uses an attached property to handle the AutoGeneratingColumn event.
HideColumnIfAutoGenerated.cs - Attribute
namespace AutoHideColumn
{
public class HideColumnIfAutoGenerated : System.Attribute
{
public HideColumnIfAutoGenerated()
{
}
}
}
DataGridExtension.cs - Attached Property
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace AutoHideColumn
{
public static class DataGridExtension
{
public static readonly DependencyProperty HideAnnotatedColumnsProperty = DependencyProperty.RegisterAttached(
"HideAnnotatedColumns",
typeof(bool),
typeof(DataGridExtension),
new UIPropertyMetadata(false, OnHideAnnotatedColumns));
public static bool GetHideAnnotatedColumns(DependencyObject d)
{
return (bool)d.GetValue(HideAnnotatedColumnsProperty);
}
public static void SetHideAnnotatedColumns(DependencyObject d, bool value)
{
d.SetValue(HideAnnotatedColumnsProperty, value);
}
private static void OnHideAnnotatedColumns(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool hideAnnotatedColumns = (bool)e.NewValue;
DataGrid dataGrid = d as DataGrid;
if (hideAnnotatedColumns)
{
dataGrid.AutoGeneratingColumn += dataGrid_AutoGeneratingColumn;
}
else
{
dataGrid.AutoGeneratingColumn -= dataGrid_AutoGeneratingColumn;
}
}
private static void dataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
PropertyDescriptor propertyDescriptor = e.PropertyDescriptor as PropertyDescriptor;
if (propertyDescriptor != null)
{
foreach (var item in propertyDescriptor.Attributes)
{
if (item.GetType() == typeof(HideColumnIfAutoGenerated))
{
e.Cancel = true;
}
}
}
}
}
}
XAML
<Window x:Class="AutoHideColumn.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AutoHideColumn"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid Name="dg" local:DataGridExtension.HideAnnotatedColumns="True">
</DataGrid>
<DataGrid Grid.Row="1" Name="dg1">
</DataGrid>
</Grid>
</Window>
CodeBehind
using System.Collections.Generic;
using System.Windows;
namespace AutoHideColumn
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.dg.ItemsSource = new List<Customer>();
this.dg1.ItemsSource = new List<Customer>();
}
}
public class Customer
{
[HideColumnIfAutoGenerated()]
public int ID { get; set; }
public string Name { get; set; }
}
}
Try this
public partial class MainWindow : Window
{
private List<string> visibleColumns;
public MainWindow()
{
InitializeComponent();
InitializeList();
visibleColumns = GetVisibleColumns();
dg.AutoGeneratingColumn += dg_AutoGeneratingColumn;
dg.ItemsSource = Templates;
}
void dg_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if(!visibleColumns.Contains(e.Column.Header.ToString()))
e.Column.Visibility=Visibility.Collapsed;
}
List<string> GetVisibleColumns()
{
return typeof(Template).GetProperties()
.Where(p =>
p.GetCustomAttributes(typeof(Visible), true)
.Where(ca => ((Visible)ca).IsVisible).Any()
).Select(s => s.Name).ToList();
}
private void InitializeList()
{
Templates = new List<Template>();
Templates.Add(new Template { County = "abc", Operator = "123", Field = "xyz" });
Templates.Add(new Template { County = "abc", Operator = "123", Field = "xyz" });
Templates.Add(new Template { County = "abc", Operator = "123", Field = "xyz" });
Templates.Add(new Template { County = "abc", Operator = "123", Field = "xyz" });
}
public List<Template> Templates { get; set; }
}
>Template Class
public class Template
{
[Visible(false)]
public string County { get; set; }
[Visible(true)]
public string Operator { get; set; }
[Visible(true)]
public string Field { get; set; }
}
>Visible Attribute
public class Visible : Attribute
{
public Visible(bool isVisible)
{
IsVisible = isVisible;
}
public bool IsVisible { get; set; }
}
>xaml
<Grid>
<DataGrid AutoGenerateColumns="True" x:Name="dg"/>
</Grid>
this would be by far the most simplistic workaround which comes close to data annotation: omit the getter and setter.
public class Template
{
public string County { get; set; }
public string Operator { get; set; }
public string Field; //This is now a field and not a property-> invisible in datadrid
}
Attributes cannot be changed at run time hence you cannot dynamically hide columns using data annotations. But it is possible to dynamically hide columns. Here is an example which demonstrates it.
XAML
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid ItemsSource="{Binding AutoGenerateColumnDetails}" CanUserAddRows="False" CanUserDeleteRows="False">
</DataGrid>
<Button Grid.Row="1" Content="Refresh" Click="Button_Click" HorizontalAlignment="Left" Margin="5"/>
<DataGrid Name="dg" Grid.Row="2" ItemsSource="{Binding TemplateList}" AutoGeneratingColumn="dg_AutoGeneratingColumn">
</DataGrid>
</Grid>
Code behind
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace AutoHideDGColumn
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ViewModel _ViewModel = null;
public MainWindow()
{
InitializeComponent();
_ViewModel = new ViewModel();
this.DataContext = _ViewModel;
}
private void dg_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (_ViewModel.AutoGenerateColumnDetails.Where(d => d.HideColumn == true).Select(d => d.PropertyName).ToList().Contains(e.PropertyName))
e.Cancel = true;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Reset to trigger AutoGeneratingColumn event
this.dg.AutoGenerateColumns = false;
this.dg.AutoGenerateColumns = true;
}
}
public class ViewModel
{
private List<Template> _TemplateList;
public List<Template> TemplateList
{
get { return _TemplateList; }
set { _TemplateList = value; }
}
private List<AutoGenerateColumnDetail> _AutoGenerateColumnDetails;
public List<AutoGenerateColumnDetail> AutoGenerateColumnDetails
{
get { return _AutoGenerateColumnDetails; }
set { _AutoGenerateColumnDetails = value; }
}
public ViewModel()
{
AutoGenerateColumnDetails = typeof(Template).GetProperties().Select(p => new AutoGenerateColumnDetail() { PropertyName = p.Name }).ToList();
TemplateList = new List<Template>()
{
new Template() { County = "Count1", Field = "Field1", Operator = "Operator1"},
new Template() { County = "Count2", Field = "Field2", Operator = "Operator2"},
new Template() { County = "Count3", Field = "Field2", Operator = "Operator3"},
};
}
}
public class Template
{
public string County { get; set; }
public string Operator { get; set; }
public string Field { get; set; }
}
public class AutoGenerateColumnDetail
{
public string PropertyName { get; set; }
public bool HideColumn { get; set; }
}
}
In my Project my selectedvalue sets correctly in viewmodel but my view not sets its selectedvalue
in xaml code:
<ComboBox ItemsSource="{Binding AllValues}" SelectedValue="{Binding SelectedValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="5,0,0,0">
in ViewModel:
public Model SelectedValue
{
get
{
return _model.Value;
}
set
{
_model.Value = value;
if (CVSCollection.View != null)
CVSCollection.View.Refresh();
RaisePropertyChanged("SelectedValue");
}
}
Try using a ICollectionView in combination with IsSynchronizedWithCurrentItem. The ICollectionView takes care of all your combobox related actions. You could also set the current selected item.
Xaml:
<ComboBox ItemsSource="{Binding AllValues}"
IsSynchronizedWithCurrentItem="True"
Width="150"
Margin="5,0,0,0"/>
ViewModel
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace ComboBoxBinding
{
public class ViewModelSpike : INotifyPropertyChanged
{
private ObservableCollection<Model> _allValues = new ObservableCollection<Model>();
private Model _selectedValue;
public ObservableCollection<Model> AllValues
{
get { return _allValues; }
set { _allValues = value; OnPropertyChanged("AllValues");}
}
public Model SelectedValue
{
get { return _selectedValue; }
set { _selectedValue = value; OnPropertyChanged("SelectedValue");}
}
public ICollectionView ModelsView { get; private set; }
public ViewModelSpike()
{
//First init / load AllValues here...
ModelsView = CollectionViewSource.GetDefaultView(AllValues);
ModelsView.CurrentChanged += OnCurrentModelChanged;
//You can also set the current selected item / initial selected item
//ModelsView.MoveCurrentTo(DesiredModel from AllValues)
}
private void OnCurrentModelChanged(object sender, EventArgs e)
{
if (ModelsView.CurrentItem == null) return;
var selectedValue = ModelsView.CurrentItem as Model;
if (selectedValue == null) return;
SelectedValue = selectedValue;
if (CVSCollection.View != null) CVSCollection.View.Refresh();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I have solved by my own as i have set local variable for selectedvalue as _selectedValue and into constructor i have set _selectedValue..
I've been at this for several times during the last couple of months, but I can't figure out how to do it.
I have a DataGrid that should show a clickable usercontrol in all columns except the first one, which should be a regular textcolumn without editing possibilities. The problem is that the number of columns has to be dynamic, there can be 2 to n ones.
Since I don't even know where to start I don't have any sample code.
If anyone could help getting me on track, I would be very grateful. The solution doesn't have to be proper MVVM or extremely fancy, it just has to work.
UPDATE 1 - A self-containing c# version.
Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.Specialized;
using System.Globalization;
namespace DynamicColumns
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded +=
(o, e) =>
{
this.PopulateItemsSource();
};
}
private void PopulateItemsSource()
{
int months = Math.Max(new Random().Next(12), 1);
this.d.ItemsSource =
new string[]
{
"John",
"Paul",
"Peter"
}.Select(t =>
MonthlyPerformance.CreateDummy(t, months)).ToList();
}
private void RePopulateButton_Click(object sender, RoutedEventArgs e)
{
this.PopulateItemsSource();
}
}
#region "Interfaces - must be in the shared between Objects & UI"
public interface IDynamicPropertiesObject
{
Dictionary<string, string> Properties { get; }
}
#endregion
#region "Objects"
public class MonthlyPerformance : IDynamicPropertiesObject
{
public string PerformerName
{
get;
set;
}
public Dictionary<string, string> Properties
{
get;
private set;
}
public static MonthlyPerformance CreateDummy(string performerName,
int months)
{
if (months < 1 || months > 12)
{
throw new ArgumentException(months.ToString());
}
Random random = new Random();
return new MonthlyPerformance()
{
PerformerName =
performerName,
Properties =
Enumerable.Range(1, months).ToDictionary(k => new DateTime(1, k, 1).ToString("MMM"), v => random.Next(100).ToString())
};
}
}
#endregion
#region "UI"
internal class DynamicPropertyValueConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
IDynamicPropertiesObject o = value as IDynamicPropertiesObject;
if (o != null)
{
return o.Properties[parameter.ToString()];
}
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class ExtendedDataGrid: DataGrid
{
public static readonly DependencyProperty IsDynamicColumnProperty =
DependencyProperty.RegisterAttached("IsDynamicColumn",
typeof(Boolean),
typeof(ExtendedDataGrid),
new PropertyMetadata(false));
private DynamicPropertyValueConverter converter = null;
public ExtendedDataGrid()
{
this.EnableColumnVirtualization = true;
this.EnableRowVirtualization = true;
this.AutoGenerateColumns = false;
}
private DynamicPropertyValueConverter Converter
{
get
{
if (this.converter == null)
{
converter = new DynamicPropertyValueConverter();
}
return this.converter;
}
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
this.ReGenerateColums();
}
private bool TryGetDynamicColumn(out DataGridColumn column)
{
column =
this.Columns.FirstOrDefault(t=>(bool)t.GetValue(ExtendedDataGrid.IsDynamicColumnProperty));
return column != null;
}
private void ClearDynamicColumns()
{
DataGridColumn column;
while (this.TryGetDynamicColumn(out column))
{
this.Columns.Remove(column);
}
}
private void ReGenerateColums()
{
this.ClearDynamicColumns();
if (this.Items.Count > 0)
{
IDynamicPropertiesObject o =
this.Items[0] as IDynamicPropertiesObject;
if (o != null)
{
foreach (KeyValuePair<string, string> property
in o.Properties)
{
DataGridTextColumn column =
new DataGridTextColumn()
{
Header = property.Key,
Binding = new Binding()
{
Converter = this.Converter,
ConverterParameter = property.Key
}
};
column.SetValue(ExtendedDataGrid.IsDynamicColumnProperty, true); // so we can remove it, when calling ClearDynamicColumns
this.Columns.Add(column);
}
}
}
}
}
#endregion
}
Markup:
<Window x:Class="DynamicColumns.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DynamicColumns"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button x:Name="RePopulateButton" Grid.Row="0" Click="RePopulateButton_Click">Re-Populate</Button>
<local:ExtendedDataGrid x:Name="d" Grid.Row="1">
<local:ExtendedDataGrid.Columns>
<DataGridTextColumn Width="Auto" Binding="{Binding PerformerName}"/>
</local:ExtendedDataGrid.Columns>
</local:ExtendedDataGrid>
</Grid>
</Window>
The follow code loads a hierarchical collection for of CompoundObjects then of strings. But unfortunately it inserts the strings at the top of the tree instead of at the bottom(which is the behavior I've always seen.
I've tried to change the order of the enumerators, the notifiers, etc and all produce the same results. I've I pre-load the list it looks normal(using the same code in the thread).
Any ideas whats going on?
CompoundObject.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
namespace ComplexTreeViewLazyLoadingTest
{
public class CompoundObject : IEnumerable<object>, INotifyCollectionChanged
{
public string Name { get; set; }
public ObservableCollection<CompoundObject> objects { get; private set; }
public ObservableCollection<string> Items { get; private set; }
void OnChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
App.Current.Dispatcher.Invoke((Action<object, NotifyCollectionChangedEventArgs>)((senderr, ee) => {
CollectionChanged(senderr, ee);
}), sender, e);
}
public CompoundObject(string name)
{
Name = name;
Items = new ObservableCollection<string>();
objects = new ObservableCollection<CompoundObject>();
Items.CollectionChanged += new NotifyCollectionChangedEventHandler(OnChanged);
objects.CollectionChanged += new NotifyCollectionChangedEventHandler(OnChanged);
}
public IEnumerator<object> GetEnumerator()
{
if (objects != null) foreach(var a in objects) yield return a;
if (Items != null) foreach (var a in Items) yield return a;
yield break;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Threading;
namespace ComplexTreeViewLazyLoadingTest
{
public partial class MainWindow : Window
{
CompoundObject c = new CompoundObject("Root");
public MainWindow()
{
InitializeComponent();
treeView.DataContext = c;
treeView.ItemsSource = c;
ThreadPool.QueueUserWorkItem(new WaitCallback(Update));
}
void Update(object data)
{
for (int i = 0; i < 10; i++)
{
Application.Current.Dispatcher.Invoke((Action<CompoundObject>)((cc) => {
c.objects.Add(cc);
}), new CompoundObject("Object " + i));
for (int j = 0; j < 5; j++)
{
Thread.Sleep(100);
Application.Current.Dispatcher.Invoke((Action<CompoundObject>)((cc) =>
{
c.objects[i].objects.Add(cc);
}), new CompoundObject("subObject " + j));
}
}
for (int i = 0; i < 8; i++)
{
Thread.Sleep(250);
Application.Current.Dispatcher.Invoke((Action<string>)((ii) =>
{
c.Items.Add("Item " + ii);
}), i.ToString());
}
}
} // MainWindow
public class DTS : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null)
{
if (item is CompoundObject)
{
return element.FindResource("CompoundTemplate") as DataTemplate;
}
if (item is int)
{
return element.FindResource("DefaultTemplate") as DataTemplate;
}
}
return null;
}
}
}
MainWindow.xaml
<Window x:Class="ComplexTreeViewLazyLoadingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ComplexTreeViewLazyLoadingTest;assembly="
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:DTS x:Key="DTS"/>
<HierarchicalDataTemplate x:Key="CompoundTemplate" ItemsSource="{Binding Path=.}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate x:Key="DefaultTemplate" ItemsSource="{Binding Path=.}">
<TextBlock Text="{Binding Path=.}" Background="Aqua" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Name="treeView" ItemTemplateSelector="{StaticResource DTS}"/>
</Grid>
</Window>
Because you're combining two collections and subscribing to the CollectionChanged directly, the change notifications are for the sub-lists instead of the 'combined' list. This means that you'll get a notification that 'string added at 0' when really you want it added at the end of the list. In order to make this work, you'll need to subscribe to the CollectionChanged for each sub-collection and implement your own CollectionChanged callback properly (to add the first collection's Count to all indices reported when adding/removing strings.)