How to refresh treeview when ItemsSource changes? - wpf

My TreeView is bound to an ObservableCollection and uses HierarchicalDataTemplates.
I get the models from a web service.
Only when users click a node in tree, a web service call will be sent to get its child items.
My App has a TabControl, TreeView is on one tabpage, the other tabpage has a datagrid - it has some data selected from treeview. When an item in datagrid is right clicked, I want to locate the item on treeview. But now the issue is when I iterate the treeview,
say, I have an item called
A.A1.A11
and my TreeView has 3 items in the first level:
A
B
C
when I locate A.A1.A11, I want to expand A, A1, and highlight A11.
When I iterate the treeview, first I find A, it matches first path of A.A1.A11,
so I send a web service request to get A's children.
Once I get that, DataContext of the TreeViewItem of A is updated, but the TreeViewItem itself is not.
So when I check A.Items, it is empty and iteratation is unable to continue.
How can I refresh the TreeView & TreeViewItem when its ItemsSource changes?
Here is the xaml for treeview definition
<TreeView x:Name="TreeRoot"
ItemsSource="{Binding RootItems}"
TreeViewItem.Expanded="TreeViewExpanded"
TreeView.SelectedItemChanged="TreeRootSelectedItemChanged"
ContextMenu="{StaticResource DataGridContextMenu}"
PreviewKeyDown="TreeViewKeyDown"
PreviewMouseLeftButtonDown="TreeViewPreviewLeftMouseButtonDown"
PreviewMouseMove="TreeViewPreviewMouseMove" >
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type m:TreeModelBase}"
ItemsSource="{Binding Children}">
<StackPanel>
<Image Source="{Binding ImageFilePath}" />
<TextBlock Text="{Binding Name}" />
<!-- other items removed for brevity -->
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}"
Value="True">
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="TextBlock.Foreground"
Value="White"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}"
Value="False">
<Setter Property="Background" Value="White"/>
<Setter Property="TextBlock.Foreground"
Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>

Does RootItems implement INotifyProperty changed and are you calling NotifyPropertyChanged? Collections need to be an ObservableCollection (not a List) for the UI to know about updates.

When populating TreeViewItem's children, you don't update DataContext, you update ItemsSource. So after the service gets the children for A, you need to assign the resulting List (or ObservableCollection) to A.ItemsSource.

In case you're using DataContext and proper data binding technique, update of the TreeViewItem can be achieved by connecting PropertyChanged to each item and deriving TreeViewItem like mentioned below (resetting DataContext and template of the tree item).
This example refreshes only 2nd level items (second condition), might be easily extended of course.
In my case ElementItem is the base class for every item in ObservableList and implements INotifyProperty.
...
itemInObservableList.PropertyChanged += Ep_PropertyChanged;
...
private void Ep_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var twi = this.pageList.ItemContainerGenerator.ContainerFromItem(
(sender as ElementItem).Parent ) as TreeViewItem;
if (twi != null)
{
var twii = twi.ItemContainerGenerator.ContainerFromItem(sender) as TreeViewItem;
if (twii != null)
{
//second level items refresh
var dc = twii.DataContext;
var tpl = twii.Template;
twii.DataContext = null;
twii.Template = null;
twii.DataContext = sender ;
twii.Template = tpl;
twii.UpdateLayout();
}
}
}

I found & managed to get it work. but it is really not optimal at all. What I did, iterate viewModel first, send web service request to get A's children, then send another request to get A1's children. This is the first pass. Second, search treeview from root and generate ItemContainers while iteration. First, search root of tree, get node A (treeviewItem), then apply template, get container from ItemContainerGenerator (see detail # How to refresh treeview when ItemsSource changes?), then I am able to find the item on treeview and expand its path. Anyone knows a better way, let me know. thanks

Related

How do I create a dynamic context menu for ListBoxItems?

I have a style that doesn't seem to be working. In spite of Snoop telling me the DataContext for the ListBoxItem is correct, nothing shows up. If it was a problem with the binding for Commands I would expect to see an empty context menu appear.
The style:
<ContextMenu x:Key="CommandsContextMenu" ItemsSource="{Binding Commands}">
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"/>
</Style>
</ContextMenu>
<Style TargetType="ListBoxItem">
<Setter Property="ContextMenu" Value="{StaticResource CommandsContextMenu}" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<Binding Path="DataContext.HasCommands" />
</DataTrigger.Binding>
</DataTrigger>
</Style.Triggers>
</Style>
The snoop DataContext:
The snoop properties showing the ContextMenu property isn't even set.
The idea here, was that without knowing any of the types, I could have a listbox item style where if the thing it was bound to has a property called HasCommands, and it was set to true, then it would set a context menu on that listbox item, bound to the Commands property.
I'm not getting any binding errors or warnings from PresentationTraceSources.DataBindingSource
Why doesn't the context menu get set?
So it turns out, that the problem was using something that inherited from ListBox
for reference here's my defined class:
// adapted from http://stackoverflow.com/questions/3350187/wpf-c-rearrange-items-in-listbox-via-drag-and-drop
// which was probably adapted from http://wpftutorial.net/DragAndDrop.html
type DragDropListBox() as self =
inherit ListBox()
which from there only hooks the following
self.PreviewMouseLeftButtonUp
self.PreviewMouseMove
override x.OnItemsSourceChanged
and in the intialization it overwrites the ItemContainerStyle as follows:
So it turns out, that the problem was using something that inherited from ListBox
for reference here's the top of my class:
// adapted from http://stackoverflow.com/questions/3350187/wpf-c-rearrange-items-in-listbox-via-drag-and-drop
// which was probably adapted from http://wpftutorial.net/DragAndDrop.html
type DragDropListBox() as self =
inherit ListBox()
which from there only hooks the following
self.PreviewMouseLeftButtonUp
self.PreviewMouseMove
override x.OnItemsSourceChanged
and in the intialization it overwrites the ItemContainerStyle as follows:
do
self.PreviewMouseLeftButtonUp.Add listBoxPreviewMouseLeftButtonUp
//self.PreviewMouseLeftButtonDown.Add listBoxPreviewMouseMove //.AddHandler (MouseButtonEventHandler previewMouseMove)
self.PreviewMouseMove.Add listBoxPreviewMouseMove
let style =
let s = Style(typeof<ListBoxItem>)
s.Setters.Add (Setter(ListBoxItem.AllowDropProperty, true))
s.Setters.Add (EventSetter(ListBoxItem.PreviewMouseLeftButtonDownEvent, MouseButtonEventHandler listBoxPreviewMouseLeftButtonDown))
s.Setters.Add (EventSetter(ListBoxItem.DropEvent, DragEventHandler listBoxItemDrop))
s
self.ItemContainerStyle <- style
now I've got to figure out if you can add two styles together.

Hide ListBoxItems not working

I have a Textbox and a ListBox
<TextBox FontSize="12pt" Text="{Binding NameFilter, UpdateSourceTrigger=PropertyChanged}" />
<ListBox x:Name="EmployeeList" ItemsSource="{Binding EmployeeList}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsVisible}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The Property of my ViewModel
public string NameFilter
{
get { return _nameFilter; }
set {
_nameFilter = value;
FilterEmployees();
}
}
FilterEmployees set the Property IsVisible of each Employee to true or false.
I have two Problems:
1.)
My Setter gets called after each user input and my EmployeeList gets filtered.
this.RaisePropertyChanged(() => this.EmployeeList);
Gets called. But I see no updates to my list.
2.)
When I manually Update my List (through another function) the Items dissapear. But they never reapear. My filter set everything to IsVisible=true, when the input string is empty (checked it inside the debugger). But not UI update happens.
What am I missing?! I used this answer, but that seems not to be my problem, since it works if I trigger another Action inside my program that updates the list:
WPF - hiding listbox items
EDIT:
I am sorting my Items (actually moving them inside the ObsservableCollection). This way the changes get visible (Employees are hidden). But this just works with hiding, they never reaper.
It seems like the whole stuff gets triggered to late. I want only employees with a "m" inside their name. I have to manually refresh the List two times. Sometimes there are still some Users left, who don't have a "m" inside their name.
You can filter the bound collection of List through CollectionView. Your implementation can be as follows : Get CollectionView for ListBox's ItemsSource and define the filter delegate. Using CollectionView and Filter you are not updating the actual ItemSource but putting a filter for what to show based on filter predicate.
private void FilterEmployees()
{
ICollectionView items = CollectionViewSource.GetDefaultView(EmployeeList);
if (items != null)
{
items.Filter = SearchFilter;
}
}
You can store as the CollectionView member variable so that you don't need to get the CollectionView again and again
public bool SearchFilter(object filterObject)
{
var filter = filterObject as <<List Box item type>>;
if (filter == null)
{
return false;
}
<<Your search logic here.......>>
}
Also, the type of EmployeeList should be ObservableCollection which I found in your edit that you did that.
Now, question here about --- do you have to search on each key stroke or you want to delay the search so that it allows user to type. So to implement this you can create a attach/dependency property to define a delay so that search function will be invoked after defined duration. You have follow the approach mentioned in this link.

Filling Contextmenu on demand

I have an entity view model. As there can be many entities in the UI, I have used a DataTemplate for representing an entity view model. This data template is used by the container control's ItemTemplate property to render the entities.
Please note this is not the conventional ListView/ListBox control. It is a Graph control with the edges and nodes being represented by the data templates.
This said, each of the nodes are of different type and when the user right clicks, the context menu of the template is getting bound to a collection provided by the view model. Following code is working fine:
<DataTemplate DataType="{x:Type model:Person}">
<Border Style="{StaticResource NodeBorderStyle}" MinWidth="200">
<Border.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextOperations}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding PlacementTarget.DataContext.HandleContextOperationCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Header}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Border.ContextMenu>
....................
</Border >
</DataTemplate>
This has a limitation, the ContextOperations (an ObservableCollection) property has to be populated when the view model gets initialized.
There may be numerous nodes and hence storing context menu items for all the nodes at loading time can be a matter of huge memory.
I want that when the user right clicks the entity (the template), the ContextOperations property of the viewmodel gets populated and then the context menu gets rendered.
This DataTemplate is kept in a resource file.
Any guidance would be appreciated.
Thank you.
regards,
Handle the MouseRightButtonUp event on you datatemplate border and create the context menu runtime like this:
private void borderMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
OpenContextMenu(sender as ComboBox);
}
private void OpenContextMenu(FrameworkElement element)
{
if (element.ContextMenu == null)
{
ContextMenu c = new ContextMenu();
//Load the ContextOperations from ViewModel
//based on the Framework element datacontext
ContextOperations.ToList().ForEach(co => c.Items.Add(new MenuItem()
{
//Create your menu item
}));
element.ContextMenu = c;
}
element.ContextMenu.PlacementTarget = element;
element.ContextMenu.IsOpen = true;
}
and remove the definition of contextmenu from the XAML.

Proper way to display a view using MVVM

I am brand new to both wpf and MVVM. I have a Mainwindow that has two views left side has a usercontrol with a listbox and the list box has a edit button inside of it. On the right I have another view that contains all my controls for viewing and editing the record. I can select an item in the list box and edit my record since using binding it automatically populates by the selectedItem object. What I want to do is when the user hits the edit button show the view on the right if they hit another button contained in the list box then show that view on the right and close the previous view. I think I am missing a big concept here since most of the examples are to simplistic and just show one view. I really dont want to have to do it in the code behind. I have looked at John smiths tab and would like to do something similur without the tabs though. Any help would be greatly appreciated.
It sounds like both views need to share the same context (ie ViewModel) then they will stay in synch automaticall by the magic of dependency properties...
I would probably try setting it up so that clicking either button (View or Edit) sets the DataContext of the right frame. The RightFrame View gets displayed using DataTemplates based on the DataContext.
So your xaml would be something like this:
<DataTemplate DataType={x:Type MyEditingViewModel}>
<!-- Editing Object View -->
</DataTemplate>
<DataTemplate DataType={x:Type MyViewingViewModel}>
<!-- Viewing Object View -->
</DataTemplate>
and your button click events would be something like this:
private void EditButton_Click(object sender, EventArgs e)
{
RightFrame.DataContext = new MyEditingViewModel((sender as Button).DataContext)
}
private void ViewButton_Click(object sender, EventArgs e)
{
RightFrame.DataContext = new MyViewingViewModel((sender as Button).DataContext)
}
So basically, what you're trying to do is have your view model decide which view it should be presented in, and make that decision in response to user choice.
How I do this sort of thing:
My view model exposes a view style property, which is an enumerable. In the view for the view model (usually a user control), I implement a DockPanel to contain each style of view. Each DockPanel is assigned a style, and the styles are defined like this:
<Style x:Key="Style_View1" TargetType="DockPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewStyle}" Value="View1">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="Style_View2" TargetType="DockPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewStyle}" Value="View2">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
(Obviously you don't need to use a DockPanel if a Grid or StackPanel is more appropriate for your scenario. And you can implement a different user control for each style of view if you want to keep your code nicely segmented.)
So as long as the value of ViewStyle is one that there's a corresponding style for, that view style will be visible. Since all of the styles set Visibility to Collapsed by default, there will only ever be at most one view style visible.
There are lots of ways of selecting the view style - create a command for each one and bind it to buttons, create a group of radio buttons and use a value converter, set the view style in response to other properties in the view model - whatever works.

ContentTemplateSelector and Choose Template based upon the ListBoxItem Selected Item

I have a XMLDataProvider Static Resource as for my Data listing some products.
I have two Controls as Master Detail scenerio
1) ListBox which lists all the Product Titles and
2) COntentControl which displays the Product details.
Now its working fine at the moment but the ContentControl is using a hard-coded Template.
What I want is to display products with different templates and ContentControl 's ContentTemplate should be picked up dynamically based upon the Product's field (TemplateName). How can I do that?
I'm stuck in writing the SelectTemplate override method in which I don't know how to access the TemplateName Property from parameter(object).
Any code would be helpful?
Are you saying that you want to look up a DataTemplate resource where the name is given by an attribute of an XmlNode? To do that, you could cast item to an XmlElement to find the value you want, and then call TryFindResource to do the resource lookup:
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var fe = container as FrameworkElement;
var element = item as XmlElement;
if (fe != null && element != null)
{
var templateName = element.GetAttribute("TemplateName");
if (templateName != null)
{
return fe.TryFindResource(templateName) as DataTemplate;
}
}
return null;
}
You could also do something similar entirely in XAML by defining a style for the ContentControl that uses DataTriggers to set the ContentTemplate:
<Style x:Key="DynamicTemplateStyle" TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding XPath=#TemplateName}" Value="FirstTemplate">
<Setter Property="ContentTemplate" Value="{StaticResource FirstTemplate}"/>
</DataTrigger>
<DataTrigger Binding="{Binding XPath=#TemplateName}" Value="SecondTemplate">
<Setter Property="ContentTemplate" Value="{StaticResource SecondTemplate}"/>
</DataTrigger>
<!-- etc. -->
</Style.Triggers>
</Style>

Resources