How to sort a treeview that has stackpanel items - c#

i tried to create somthing to quickly locate and watch files. So i created a TreeView that has StackPanels as Items. A StackPanel contains an Image an a Label.
private TreeViewItem createFile(string Name, string soureFile)
{
TreeViewItem tvi = new TreeViewItem();
StackPanel sp = new StackPanel();
Image i = new Image();
Label l_Text = new Label();
Label l_FileName = new Label();
l_FileName.Content = soureFile;
l_FileName.Width = 0;
l_Text.Content = Name;
System.Drawing.Bitmap dImg = (System.Drawing.Bitmap)Properties.Resources.ResourceManager.GetObject("Picture");
MemoryStream ms = new MemoryStream();
dImg.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
BitmapImage bImg = new BitmapImage();
bImg.BeginInit();
bImg.StreamSource = new MemoryStream(ms.ToArray());
bImg.EndInit();
i.Source = bImg;
i.Height = 20;
i.Width = 20;
sp.Name = "SP_File";
sp.Orientation = Orientation.Horizontal;
sp.Children.Add(i);
sp.Children.Add(l_Text);
sp.Children.Add(l_FileName);
tvi.Header = sp;
return tvi;
}
One can create logical folders (just for creating a structure) and add files and other folders to the folders (just references to to the actual file on the hdd). This worked fine until i tried to sort the TreeView. I read stuff on on Sorting TreeViews with
SortDescriptions.Add(new SortDescription("Header", ListSortDirection.Ascending));
Obviously this doesn't work for me since i cant exchange "Header" with "Header.StackPanel.Label.Text"
As I read a little bit further it seems I used the wrong approach to the whole thing by not using MVVM (Numerically sort a List of TreeViewItems in C#).
Since I have litte to no experience with MVVM can someone explain to me how it is best do this with MVVM? I use a List of "watchedFile" to keep the files and folders.
I basically have the the following class for a file
class watchedFile
{
public string name { get; private set; }
public string path { get; private set; }
public List<string> tags { get; private set; }
public watchedFile(string Name, string Path, List<string> Tags)
{
name = Name;
path = Path;
tags = Tags;
}
}
If path is null or empty its a folder. The TreeViewItem has a little Image which shows a little "folder" or "picture", a label which shows the "watchedFile.name" and an invisible label which contains the watchedfile.path (which is only shown as a tooltip). I guess I should do this with DataBinding so I dont need to add an invisible Label.
Questions:
How can I solve the task using MVVM?
How/where can I bind the Image to the TreeViewItem when I have just the wacthedFile.path to distinguish?
How do I sort the watched items?
How do I keep track of the TreeView level (so i can rebuild the structure from a saved file)?
Is there a way to sort a TreeView with StackPanel Items without using MVVM/Data-Binding?
Any help is highly appreciated.

Here's how to do this MVVM fashion.
First, write viewmodel classes. Here, we've got a main viewmodel that has a collection of WatchedFile instances, and then we've got the WatchedFile class itself. I've also decided to make Tag a class, instead of just using strings. This lets us write data templates in the XAML that explicitly and exclusively will be used to display Tag instances, rather than strings in general. The UI is full of strings.
The notification properties are tedious to write if you don't have a snippet. I have snippets (Steal them! They're not nailed down!).
Once you've got this, sorting is no big deal. If you want to sort the root level items, those are WatchedFile.
SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
But we'll do that in XAML below.
Serialization is simple, too: Just make your viewmodel classes serializable. The important thing here is that your sorting and serialization don't have to care what's in the treeview item templates. StackPanels, GroupBoxes, whatever -- it doesn't matter at all, because your sorting and serialization code just deals with your data classes, not the UI stuff. You can change the visual details in the data templates radically without having to worry about it affecting any other part of the code. That's what's nice about MVVM.
Viewmodels:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace WatchedFile.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class WatchedFile : ViewModelBase
{
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
#region Path Property
private String _path = default(String);
public String Path
{
get { return _path; }
set
{
if (value != _path)
{
_path = value;
OnPropertyChanged();
}
}
}
#endregion Path Property
#region Tags Property
private ObservableCollection<Tag> _tags = new ObservableCollection<Tag>();
public ObservableCollection<Tag> Tags
{
get { return _tags; }
protected set
{
if (value != _tags)
{
_tags = value;
OnPropertyChanged();
}
}
}
#endregion Tags Property
}
public class Tag
{
public Tag(String value)
{
Value = value;
}
public String Value { get; private set; }
}
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Populate();
}
public void Populate()
{
// Arbitrary test info, just for display.
WatchedFiles = new ObservableCollection<WatchedFile>
{
new WatchedFile() { Name = "foobar.txt", Path = "c:\\testfiles\\foobar.txt", Tags = { new Tag("Testfile"), new Tag("Text") } },
new WatchedFile() { Name = "bazfoo.txt", Path = "c:\\testfiles\\bazfoo.txt", Tags = { new Tag("Testfile"), new Tag("Text") } },
new WatchedFile() { Name = "whatever.xml", Path = "c:\\testfiles\\whatever.xml", Tags = { new Tag("Testfile"), new Tag("XML") } },
};
}
#region WatchedFiles Property
private ObservableCollection<WatchedFile> _watchedFiles = new ObservableCollection<WatchedFile>();
public ObservableCollection<WatchedFile> WatchedFiles
{
get { return _watchedFiles; }
protected set
{
if (value != _watchedFiles)
{
_watchedFiles = value;
OnPropertyChanged();
}
}
}
#endregion WatchedFiles Property
}
}
Code behind. Note I only added one line here to what the wizard gave me.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;
namespace WatchedFile
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModels.MainViewModel();
}
}
}
And lastly the XAML:
<Window
x:Class="WatchedFile.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:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:WatchedFile"
xmlns:vm="clr-namespace:WatchedFile.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<CollectionViewSource
x:Key="SortedWatchedFiles"
Source="{Binding WatchedFiles}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<TreeView
ItemsSource="{Binding Source={StaticResource SortedWatchedFiles}}"
>
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type vm:WatchedFile}"
ItemsSource="{Binding Tags}"
>
<TextBlock
Text="{Binding Name}"
ToolTip="{Binding Path}"
/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type vm:Tag}"
>
<TextBlock
Text="{Binding Value}"
/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
The XAML is less than obvious. TreeView.Resources is in scope for any child of the TreeView. The HierarchicalDataTemplates don't have an x:Key property, which makes them implicit. That means that when the TreeView's items are instantiated, each of the root items will have a WatchedFile class instance for its DataContext. Since WatchedFile has an implicit data template, that will be used to fill in its content. TreeView is recursive, so it uses HierarchicalDataTemplate instead of regular DataTemplate. HierarchicalDataTemplate adds the ItemSource property, which tells the item where to look for its children on its DataContext object. WatchedFile.Tags is the ItemsSource for the root-level tree items. Those are Tag, which has its own implicit HierarchicalDataTemplate. It doesn't have children so it doesn't use HierarchicalDataTemplate.ItemsSource.
Since all our collections are ObservableCollection<T>, you can add and remove items from any collection at any time and the UI will update automagically. You can do the same with the Name and Path properties of WatchedFile, because it implements INotifyPropertyChanged and its properties raise PropertyChanged when their values change. The XAML UI subscribes to all the notification events without being told, and does the right thing -- assuming you've told it what it needs to know to do that.
Your codebehind can grab SortedWatchedFiles with FindResource and change its SortDescriptions, but this makes more sense to me, since it's agnostic about how you're populating the treeview:
<Button Content="Re-Sort" Click="Button_Click" HorizontalAlignment="Left" />
<!-- ... snip ... -->
<TreeView
x:Name="WatchedFilesTreeView"
...etc. as before...
Code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
var cv = CollectionViewSource.GetDefaultView(WatchedFilesTreeView.ItemsSource);
cv.SortDescriptions.Clear();
cv.SortDescriptions.Add(
new System.ComponentModel.SortDescription("Name",
System.ComponentModel.ListSortDirection.Descending));
}

or the non MVVM solution would have been....
I see your header is a StackPanel with 2 children and you whish to sort on the Content of the label, which is the 2nd child
You would access the label child as an array of position [1] since arrays are 0 based.
TreeView1.Items.SortDescriptions.Clear();
TreeView1.Items.SortDescriptions.Add(new SortDescription("Header.Children[1].Content", ListSortDirection.Ascending));
TreeView1.Items.Refresh();

Related

DataGrid bound to List not letting user add new row

I know that this question is duplicate of several others because I have been studying those "several others." And I have been unable to figure out where I'm messing up.
This post is specifically about:
C#
WPF
DataGrid
List<>
Binding DataGrid to the List<>
When I run my code, DataGrid allows me to change and delete rows. DataGrid does not allow me to add a row. Why?
Xaml:
<Window
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:myNamespace"
xmlns:Schema="clr-namespace:System.Xml.Schema;assembly=System.Xml.ReaderWriter" x:Class="myNamespace.MainWindow"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="600">
<Grid>
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn x:Name="col_id" Width="200" Header="Col ID" Binding="{Binding value_id}" />
<DataGridTextColumn x:Name="col_1" Width="200" Header="Col One" Binding="{Binding value_1}" />
<DataGridTextColumn x:Name="col_2" Width="200" Header="Col Two" Binding="{Binding value_2}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
Code Behind:
using System.Collections.Generic;
using System.Windows;
namespace myNamespace
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
init_item_list();
dataGrid.ItemsSource = ItemList;
}
public List<DataItem> ItemList = new List<DataItem>();
public void init_item_list()
{
ItemList.Add(new DataItem(1, "one", "i"));
ItemList.Add(new DataItem(2, "two", "ii"));
ItemList.Add(new DataItem(3, "three", "ii"));
}
public class DataItem
{
public DataItem(int id, string val_1, string val_2)
{
value_id = id;
value_1 = val_1;
value_2 = val_2;
}
private int _value_id;
public int value_id
{
get { return _value_id; }
set { _value_id = value; }
}
private string _value_1;
public string value_1
{
get { return _value_1; }
set { _value_1 = value; }
}
private string _value_2;
public string value_2
{
get { return _value_2; }
set { _value_2 = value; }
}
}
}
}
I believe I saw rows added from the user interface when DataGrid is bound to a List. However, I don't see it working with my code here.
Efraim Newman commented that could be the problem is using List. I have seen mention in other posts that ObservableCollection works better than List for DataGrid binding.
In the code behind, I changed
public List<DataItem> ItemList = new List<DataItem>();
to
public ObservableCollection<DataItem> ItemList = new ObservableCollection<DataItem>();
Before the change (using List), the running DataGrid looked like this:
After the change (using ObservableCollection), the running DataGrid looked like this:
In either case, there is no blank row by which to add a new record.
I do not see any difference using ObservableCollection instead of List. DataGrid still does not allow me to add a record.
I think the advantage of using ObservableCollection is that ObservableCollection notifies (causes DataGrid) to update whenever there is a change in ObservableCollection. That does not seem to be the problem here.
Here, the problem is that DataGrid is not accepting any new entries.
In Xaml I tried explicitly setting CanUserAddRows to True and even to False.
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False" CanUserAddRows="True" >
The Answer
#mm8 found my problem: I needed to add a default, parameterless constructor. I added:
public DataItem() { }
The Xaml remains as it was originally. Note I am now binding to List.
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
namespace myNamespace
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
init_item_list();
dataGrid.ItemsSource = _itemList;
}
private List<DataItem> _itemList = new List<DataItem>();
public List<DataItem> ItemList
{
get { return _itemList; }
set { _itemList = value; }
}
public void init_item_list()
{
_itemList.Add(new DataItem(1, "one", "i"));
_itemList.Add(new DataItem(2, "two", "ii"));
_itemList.Add(new DataItem(3, "three", "ii"));
}
public class DataItem
{
public DataItem() { }
public DataItem(int id, string val_1, string val_2)
{
value_id = id;
value_1 = val_1;
value_2 = val_2;
}
private int _value_id;
public int value_id
{
get { return _value_id; }
set { _value_id = value; }
}
private string _value_1;
public string value_1
{
get { return _value_1; }
set { _value_1 = value; }
}
private string _value_2;
public string value_2
{
get { return _value_2; }
set { _value_2 = value; }
}
}
I think an example where you cannot use List is where you will change List directly and need DataGrid to show the changes.
I have tested this code using both List and ObservableCollection. It now works from adding the default, parameterless constructor.
A List<T> doesn't implement the INotifyCollectionChanged interface that is required for the view to be able to subscribe to items being added or removed and refresh the control automatically.
The only built-in class that implements this interface is ObservableCollection<T>. If you change the type of ItemList, it should work as expected:
public ObservableCollection<DataItem> ItemList = new ObservableCollection<DataItem>();
You should also consider either making ItemList a public property or rename it to _itemList and make it a private field:
private readonly ObservableCollection<DataItem> _itemList = new ObservableCollection<DataItem>();
For the DataGrid to display a blank row that lets you add an item to the source collection, your DataItem class must define a default parameterless constructor:
public DataItem() { }
It seems that the problem is that you are using List which doesn't tell WPF when it gets updated. you should use ObservableCollection instead.

C# WPF DataBinding - Data not Updating

There are several "WPF Databinding Not Updating" questions on here, and I've read through them all, including the top-ranked one:
WPF DataBinding not updating?
However, I'm swapping out entire objects in my situation, and what's worse is that when I try to create a reproducible version of what I'm doing, it works in that version:
<Window x:Class="WpfApp2.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:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Label Content="Contact Address" />
<TextBox x:Name="txtContactAddress" Text="{Binding Path=Contact.Address, Mode=OneWay}" />
<Label Content="Organization Address" />
<TextBox x:Name="txtOrgAddress" Text="{Binding Path=Organization.Address, Mode=OneWay}" />
<Button Content="Next Contact" Click="Button_Click" />
</StackPanel>
</Window>
Code-Behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;
namespace WpfApp2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public RecordWrapper CurrentRecord;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
CurrentRecord = new RecordWrapper(new ContactData());
this.DataContext = CurrentRecord;
}
}
public class RecordWrapper : INotifyPropertyChanged
{
private ContactData _Contact;
public ContactData Contact
{
get { return _Contact; }
set
{
_Contact = value;
OnPropertyChanged("Contact");
Organization = _Contact.Organization;
}
}
private OrganizationData _Organization;
public OrganizationData Organization
{
get { return _Organization; }
set
{
_Organization = value;
OnPropertyChanged("Organization");
}
}
public RecordWrapper(ContactData contactData)
{
Contact = contactData;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
#endregion
}
public class ContactData
{
public string Address { get; set; }
public OrganizationData Organization { get; set; }
public ContactData()
{
Address = new Random().Next(1000, 9999) + " Contact Street";
Organization = new OrganizationData();
}
}
public class OrganizationData
{
public string Address { get; set; }
public OrganizationData()
{
Address = new Random().Next(1000, 9999) + " Org Street";
}
}
}
So this particular sample works as expected - when I click on the "Next Contact" button, it generates a new RecordWrapper object that contains a new ContactData and new OrganizationData object, and then assigns the new object to DataContext and then I see the textboxes update properly.
Since I'm replacing the entire object in the DataContext, I can even ditch the property notifications and reduce the RecordWrapper down to just:
public class RecordWrapper
{
public ContactData Contact { get; set; }
public OrganizationData Organization { get; set; }
public RecordWrapper(ContactData contactData)
{
Contact = contactData;
Organization = Contact.Organization;
}
}
...and it still works great.
My real code, however, reflects the first example.
In the real code, it seems like updating this.DataContext with a new object doesn't work. Nothing is updated, but there are no errors, either. Setting the path in XAML doesn't work, either. In fact, the only way I can get it to work is to use code-behind to set the bindings directly to the data object:
BindingOperations.SetBinding(txtContactAddress, TextBox.TextProperty, new Binding() { Source = this.RecordWrapper, Path = new PropertyPath("Contact.Address"), Mode = BindingMode.OneWay });
When I do this, I can see it working, but that's obviously missing the point of binding if I have to run code on each field each time to update the bindings.
I'm stumped because the real code is nearly identical to the example aside from having more fields, different field names, and differences that have nothing to do with data or binding. When I click on a button, this is the exact code:
this.Data = new DataBindings(CurrentContact.FetchFake());
this.DataContext = this.Data;
The FetchFake() method generates a new CurrentContact object with some fake data, and I have verified that after that first line runs, this.Data.CurrentContact.Login is populated and is a read-only property of the CurrentContact object (not a field).
...and one of the XAML textboxes looks like this:
<TextBox x:Name="txtAccountNumber" Grid.Column="1" Grid.Row="1" Width="Auto" Text="{Binding Path=CurrentContact.Login, Mode=OneWay}"/>
The result is blank. I have also tried variations on order (e.g. setting DataContext to this.Data first, then setting the this.Data.CurrentContact property and letting the property-changed notification kick off, but still no luck. But if I run:
BindingOperations.SetBinding(tabAccount.txtAccountNumber, TextBox.TextProperty, new Binding() { Source = this.Data, Path = new PropertyPath("CurrentContact.Login"), Mode = BindingMode.OneWay });
...then I'll see the value show up.
Based on these symptoms, and the fact that my first example works but the real code doesn't, can anyone think of any place I should look for some culprits?
Ughhhhh. Figured it out. I had a REALLY old line of code in there that changed the DataContext of a container element that was between the Window and the Textbox, which interrupted the correct binding. I thought I had removed all of it, but hadn't. It DID throw an error, as Erno de Weerd had suggested, but the error was lost among some of the startup logging. We can close this one out.

Make certain rows unselectable in DataGrid from ViewModel

I am building a WPF client following the MVVM pattern. The client uses a wpf DataGrid to display some data with some complex grouping requirements. Rather than try to find a way to display all this grouping within the grid, I simply generate synthetic entries for group headers and footers.
All this works ok but it leaves me with the problem that I have rows in my DataGrid that should not be selectable. I was hoping I could control the selection from the View Model but that doesn't seem to be working. I have created a sample project that illustrates the problem.
App.xaml
<Application x:Class="TestMVVM.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TestMVVM.ViewModel"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<vm:MainViewModel x:Key="MainViewModel"/>
</ResourceDictionary>
</Application.Resources>
</Application>
MainWindow.xaml
<Window x:Class="TestMVVM.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" DataContext="{StaticResource MainViewModel}">
<Grid>
<DataGrid ItemsSource="{Binding MyItems}" SelectedItem="{Binding MySelectedItem}" IsSynchronizedWithCurrentItem="True"
AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single" SelectionUnit="FullRow">
<!-- In the real app I use Style triggers here to highlight title and total rows and make them unselectable from the mouse,
but you can still select them via the keyboard and when the grid is initially displayed it's on an invalid row -->
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Count" Binding="{Binding Count}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
MyItem.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestMVVM.Model
{
public class MyItem
{
public MyItem(string name, int count)
{
Name = name;
Count = count;
}
public string Name { get; private set; }
public int Count { get; private set; }
}
public class MyItemTitle
{
public MyItemTitle(string name)
{
Name = name;
}
public string Name { get; private set; }
}
public class MyItemTotal
{
public MyItemTotal(string name, int total)
{
Name = name;
Count = total;
}
public string Name { get; private set; }
public int Count { get; private set; }
}
}
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using TestMVVM.Model;
namespace TestMVVM.ViewModel
{
public class MainViewModel : INotifyPropertyChanged
{
private object _mySelectedItem;
private int _lastIndex = -1;
public MainViewModel()
{
var myItems = new List<object> {
new MyItemTitle("Top Title"),
new MyItemTitle("Subtitle"),
new MyItem("Real Item", 1),
new MyItem("Second Real Item", 4),
new MyItemTotal("Subtitle totals", 5),
new MyItem("Third Real Item", 11),
new MyItemTotal("Top Title Totals", 16)
};
// Initially I used a List<> here but I thought maybe ObservableCollection
// might make a difference. As you can see, it does not.
MyItems = new ObservableCollection<object>(myItems);
}
public IList<object> MyItems { get; private set; }
public object MySelectedItem
{
get { return _mySelectedItem; }
set
{
SetValidItem(value);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void SetValidItem(object value)
{
var newIndex = MyItems.IndexOf(value);
if (newIndex == _lastIndex) return; // no change, doubt this can happen
if (IsItemValid(value))
{
_mySelectedItem = value;
_lastIndex = newIndex;
return; // selection worked as DataGrid expected
}
if (newIndex > _lastIndex) // selection going down the grid
{
for (var i = newIndex + 1; i < MyItems.Count; ++i)
{
if (IsItemValid(MyItems[i]))
{
_mySelectedItem = MyItems[i];
_lastIndex = i;
}
}
}
else if (newIndex < _lastIndex) // selection going up the grid
{
for (var i = newIndex - 1; i > -1; --i)
{
if (IsItemValid(MyItems[i]))
{
_mySelectedItem = MyItems[i];
_lastIndex = i;
}
}
}
// three possible scenarios when we get here:
// 1) selection went up higher than selected in grid
// 2) selection went down lower than selected in grid
// 3) no valid higher/lower item was found so we keep the previous selection
// in any of those cases we need to raise an event so the grid knows
// that SelectedItem has moved
RaiseOnMySelectedItemChanged();
// I checked in the debugger and the event fired correctly and the DataGrid
// called the getter again with the correct value. The grid seemed to ignore
// it though so I thought I could force the issue with this. This doesn't seem
// to do anything either (I even tried collectionView.MoveCurrentToLast())
var collectionView = CollectionViewSource.GetDefaultView(MyItems);
collectionView.MoveCurrentTo(_mySelectedItem);
}
private bool IsItemValid(object value)
{
return value is MyItem;
}
private void RaiseOnMySelectedItemChanged()
{
var handler = PropertyChanged;
if (handler != null)
{
var args = new PropertyChangedEventArgs("MySelectedItem");
handler(this, args);
}
}
}
}
1.One way to approach this is to extend the DataGrid. Then in the DataGridExtended determine whether certain Row or Cell needs to be skipped from selection (by maybe moving the focus to the next cell or row). The processing part is easy. What's harder is to get the cell or the row. Here's a post how to do that:
How to get a cell from DataGrid?
In your ViewModel or model create properties that mark an item as not-selectable, and respond to it in the DataGridExtended.
2.There is also an interesting post on the subject that might work for you, or be an additional helper. It basically creates a style to make the row unfocusable. Making a row non-focusable in a WPF datagrid
3.you can directly respond and cancel selection events in code behind. Together with (part 2.) this might do the trick. Here's another link how to get started: http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing&referringTitle=Tips%20%26%20Tricks

Filtering animals using wpf radiobuttons

I am playing with a sample WPF application that is tiered in a Model-View-Presenter manner. The Model is a collection of Animals which is displayed in a view via binding to properties in a presenter class. The XAML has an items control that displays all the animals in a model.
The model class has a boolean attribute called 'IsMammal'. I want to introduce a filter in the XAML in the form of a radio button group that filters the collection of animals based on the 'IsMammal' attribute. Selection of the radiobutton 'Mammals' updates the items control with all the Animals that have the 'IsMammal' value set to true and when the value is toggled to 'Non-Mammals', the display is updated with all animals that have that particular boolean set to false. The logic to do the filtering is extremely simple. What is troubling me is the placement of the logic. I don't want any logic embedded in the *.xaml.cs. I want the toggling of the radiobutton to trigger a logic in the presenter that tapers my animal collection to be rebound to the display.
Some guidance here will be extremely appreciated.
Thanks
I suggest you do the following in your Presenter:
=> Create a ListCollectionView field. Set it to be equal to the "Default Collection View" of your collection, and use it as the ItemsSource for the list control. Something like:
public class Presenter()
{
private ListCollectionView lcv;
public Presenter()
{
this.lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(animalsCollection);
listControl.ItemsSource = this.lcv;
}
}
=> In the handler/logic for the RadioButton's corresponding event, filter the ListCollectionView. Something like:
void OnCheckedChanged()
{
bool showMammals = radioButton.IsChecked;
this.lcv.Filter = new Predicate((p) => (p as Animal).IsMammal == showMammals);
this.lcv.Refresh();
}
Hope this helps.
EDIT:
While doing this is possible using MVP, using MVVM should be a better choice, IMHO (and as mentioned by the other answer). To help you out, I wrote a sample that implements your requirements via MVVM. See below:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Vertical">
<RadioButton x:Name="rb1" GroupName="MyGroup" Content="IsMammal = true" Checked="rb1_Checked"/>
<RadioButton x:Name="rb2" GroupName="MyGroup" Content="IsMammal = false" Checked="rb2_Checked"/>
<ListBox ItemsSource="{Binding Path=AnimalsCollectionView}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Window>
Code-behind:
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.ObjectModel;
using System.Threading;
using System.ComponentModel;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void rb1_Checked(object sender, RoutedEventArgs e)
{
(this.DataContext as ViewModel).UpdateFilter(true);
}
private void rb2_Checked(object sender, RoutedEventArgs e)
{
(this.DataContext as ViewModel).UpdateFilter(false);
}
}
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Animal> animals = new ObservableCollection<Animal>();
private ListCollectionView animalsCollectionView;
public ListCollectionView AnimalsCollectionView
{
get { return this.animalsCollectionView; }
}
public void UpdateFilter(bool showMammals)
{
this.animalsCollectionView.Filter = new Predicate<object>((p) => (p as Animal).IsMammal == showMammals);
this.animalsCollectionView.Refresh();
}
public ViewModel()
{
this.animals.Add(new Animal() { Name = "Dog", IsMammal = true });
this.animals.Add(new Animal() { Name = "Cat", IsMammal = true });
this.animals.Add(new Animal() { Name = "Bird", IsMammal = false });
this.animalsCollectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(this.animals);
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}
public class Animal : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return this.name; }
set
{
this.name = value;
this.OnPropertyChanged("Name");
}
}
private bool isMammal;
public bool IsMammal
{
get { return this.isMammal; }
set
{
this.isMammal = value;
this.OnPropertyChanged("IsMammal");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}
}
I came from an MVP background before learning WPF and I have come to find that adapting the MVP pattern to WPF is a difficult exercise at best. The "proper" (read, least frustrating) approach is to utilize the Model-View-ViewModel (MVVM) pattern, which makes heavy use of the databinding features of WPF to minimize the amount of code that ends up the view-behind file.
In the simplest case, you would end up with two properties on your ViewModel:
bool FilterMammals
ObservableCollection MammalsToDisplay
In the XAML, You would bind the first to your radio button group and the second to the ItemsSource of the ListBox. The WPF databinding framework will call your property setter whenever the radiobutton group value changes, and in here you can filter and then update the list of items.
I'm not sure what your familiarity with MVVM is, so I'll stop here. Let me know if more detail would help :).

Update binding of TreeViewItem using code? (XAML / WPF)

I have a TreeView whose ItemsSource is set to a Model I have. 3 levels deep is an object whose state can change and, rather than write a View and ViewModel for each stage (very lengthy business) I wanted to just update this using code.
So basically I have an event that is fired once my model updates, I catch it then find the TreeViewItem associated with this bit of data. My issue now is I have NO idea on how to update the binding on it to reflect the new value!
Can anyone help?
I know this isn't best practice but I really don't want to have to waste time writing a massive amount of code to just update one thing.
Thanks
Chris
Are you sure it wouldn't be easier to implement INotifyPropertyChanged on the relevant class?
This example works, though it's only two (not 3) levels deep. It shows a simple 2-level hierarchical treeview with parent items A, B, and C, with numbered children (A.1, B.1, etc). When the Rename B.1 button is clicked, it renames B.1 to "Sylvia".
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace UpdateVanillaBindingValue
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
private DataClass _data;
public Window1()
{
InitializeComponent();
var data = CreateData();
DataContext = _data = data;
}
private DataClass CreateData()
{
return new DataClass
{
Parents=new List<Parent>
{
new Parent{Name="A",Children=new List<Child>{new Child{Name="A.0"},new Child{Name="A.1"}}},
new Parent{Name="B",Children=new List<Child>{new Child{Name="B.0"},new Child{Name="B.1"},new Child{Name="B.2"}}},
new Parent{Name="C",Children=new List<Child>{new Child{Name="C.0"},new Child{Name="C.1"}}}
}
};
}
private void Rename_Click(object sender, RoutedEventArgs e)
{
var parentB = _data.Parents[1];
var parentBItem = TheTree.ItemContainerGenerator.ContainerFromItem(parentB) as TreeViewItem;
parentB.Children[1].Name = "Sylvia";
var parentBItemsSource = parentBItem.ItemsSource;
parentBItem.ItemsSource = null;
parentBItem.ItemsSource = parentBItemsSource;
}
}
public class DataClass
{
public List<Parent> Parents { get; set; }
}
public class Parent
{
public string Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public string Name { get; set; }
}
}
<Window x:Class="UpdateVanillaBindingValue.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="ChildTemplate">
<TextBlock Margin="50,0,0,0" Text="{Binding Name}" />
</DataTemplate>
<HierarchicalDataTemplate x:Key="ParentTemplate" ItemsSource="{Binding Children}" ItemTemplate="{StaticResource ChildTemplate}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView x:Name="TheTree" ItemsSource="{Binding Parents}" ItemTemplate="{StaticResource ParentTemplate}" />
<Button VerticalAlignment="Bottom" HorizontalAlignment="Center" Content="Rename B.1" Click="Rename_Click" />
</Grid>
</Window>
This is a hack, but it re-evaluates the DataTemplate every time it's ItemsSource property changes.
Ideally, you would implement INotifyPropertyChanged on your model object class that this TreeViewItem is bound to, and have it fire the PropertyChanged event when that value changes. In fact, you should be careful that you aren't incurring a memory leak because it doesn't: Finding memory-leaks in WPF Applications.
Sounds like you might need to take a look at the UpdateSource and UpdateTarget methods.
MSDN reference for UpdateSource
Although I'm not totally sure this will work when you've actually bound the TreeView's ItemSource to a hierarchical data structure, but it's where I would start investigating.

Categories

Resources