What's a Good Way to Obtain ListView Item Containers? - wpf

My question is for UWP but the solution might be the same in WPF so I tagged that as well.
I'm trying to implement this extension method in a custom GridView and ListView so that when a selection changes, the selected item will always smoothly animate into view.
The extension method works great. However obtaining the UIElement container to send it does not work so great.
ListView.Items is bound to a collection in a ViewModel. So ListView.Items are not UIElements, but rather data objects. I need the SelectedItem's corresponding UIElement container.
First I tried this:
void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_scrollViewer != null && this.ItemsPanelRoot != null && this.Items.Count > 0)
{
var selectedListViewItem = this.ItemsPanelRoot.Children[this.SelectedIndex];
if (selectedListViewItem != null)
{
_scrollViewer.ScrollToElement(selectedListViewItem);
}
}
}
This works at first but is actually no good. The indices of 'ListView.ItemsPanelRoot.Children' eventually start to diverge from 'ListView.Items' as the layout is updated dynamically.
Then I tried this, which so far seems to work fine:
void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_scrollViewer != null && this.Items.Count > 0)
{
var selectedListViewItem = this.ItemsPanelRoot.FindDescendants<ListViewItem>()
.Where(x => x.Content == this.SelectedItem).FirstOrDefault();
if (selectedListViewItem != null)
{
_scrollViewer.ScrollToElement(selectedListViewItem);
}
else
{
throw new Exception();
}
}
}
The problem is that it seems incredibly expensive to do that query each time and also unsafe because there's no assurance that the container is available yet. I feel (hope) I'm missing something and that there's a proper way to do this.
Note: 'FindDescendants' is an extension method from Windows UI Toolkit, does the same thing as VisualTreeHelper.

ItemsControls have an Item​Container​Generator property, which holds an Item​Container​Generator object that you can use to get item containers for items and item indexes and the like.
E.g.
var container = listView.ItemContainerGenerator.ContainerFromItem(item);
In UWP, the ContainerFromItem and similar methods are also directly available in the ItemsControl class, so that you could write
var container = listView.ContainerFromItem(item);

Related

How to set scroll position from view model with caliburn.micro?

I have a ListBox in my view, bound to a collection that is dynamically growing. I would like the scroll position to follow the last added item (which is appended to the bottom of the list). How can I achieve this with Caliburn.Micro?
An alternative could be to use the event aggregator to publish a message to the view.
Something like:
Aggregator.Publish(ItemAddedMessage<SomeItemType>(itemThatWasJustAdded));
and in the view:
public class SomeView : IHandle<ItemAddedMessage<SomeItemType>>
{
public void Handle(ItemAddedMessage<SomeItemType> message)
{
// Implement view specific behaviour here
}
}
It depends on what your requirements are but at least then the view is responsible for display concerns and you can still test the VM
Also you could just implement the code solely in the view - since it appears to be a view concern (e.g. using the events that listbox provides)
A behaviour would also be useful but maybe one that's a little less coupled to your types - e.g. a generic behaviour SeekAddedItemBehaviour which hooks listbox events to find the last item. Not sure if the listbox exposes the required events, but worth a look
EDIT:
Ok this may work full stop - you should be able to just attach this behaviour to the listbox and it should take care of the rest:
public class ListBoxSeekLastItemBehaviour : System.Windows.Interactivity.Behavior<ListBox>
{
private static readonly DependencyProperty ItemsSourceWatcherProperty = DependencyProperty.Register("ItemsSourceWatcher", typeof(object), typeof(ListBoxSeekLastItemBehaviour), new PropertyMetadata(null, OnItemsSourceWatcherPropertyChanged));
private ListBox _listBox = null;
private static void OnItemsSourceWatcherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxSeekLastItemBehaviour source = d as ListBoxSeekLastItemBehaviour;
if (source != null)
source.OnItemsSourceWatcherPropertyChanged();
}
private void OnItemsSourceWatcherPropertyChanged()
{
// The itemssource has changed, check if it raises collection changed notifications
if (_listBox.ItemsSource is INotifyCollectionChanged)
{
// if it does, hook the CollectionChanged event so we can respond to items being added
(_listBox.ItemsSource as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ListBoxSeekLastItemBehaviour_CollectionChanged);
}
}
void ListBoxSeekLastItemBehaviour_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Count > 0)
{
// If an item was added seek it
ScrollIntoView(e.NewItems[0]);
}
}
protected override void OnAttached()
{
base.OnAttached();
// We've been attached - get the associated listbox
var box = this.AssociatedObject as ListBox;
if (box != null)
{
// Hold a ref
_listBox = box;
// Set a binding to watch for property changes
System.Windows.Data.Binding binding = new System.Windows.Data.Binding("ItemsSource") { Source = _listBox; }
// EDIT: Potential bugfix - you probably want to check the itemssource here just
// in case the behaviour is applied after the original ItemsSource binding has been evaluated - otherwise you might miss the change
OnItemsSourceWatcherPropertyChanged();
}
}
private void ScrollIntoView(object target)
{
// Set selected item and try and scroll it into view
_listBox.SelectedItem = target;
_listBox.ScrollIntoView(target);
}
}
You probably want to tidy it up a bit and also make sure that the event handler for CollectionChanged is removed when the ItemsSource changes.
Also you might want to call it SeekLastAddedItemBehaviour or SeekLastAddedItemBehavior - I tend to keep the US spelling since it matches Microsoft's spelling. I think SeekLastItem sounds like it will scroll to the last item in the list rather than the last added item
You could reference the view in the view model using GetView(). That also couples the view and view model.
var myView = GetView() as MyView;
myView.MyListBox.DoStuff
Another option is to create a behavior. This is an example of how to use a behavior to expand a TreeView from the view model. The same could be applied to a ListBox.
Actually, there is an easier way to achieve this, without any of the above.
Just extend your Listbox with the following:
namespace Extensions.Examples {
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
}
Then in Xaml, Declare the Location of your extension class as so:
xmlns:Extensions="clr-namespace:Extensions.Examples"
And when you create your listbox, instead of using
<Listbox></Listbox>
Just use your extended class
<Extensions:ScrollingListBox></Extensions:ScrollingListBox>

Implement 'Delete Item' command for multiple ListView controls

I have multiple ListViews in an MVVM WPF application, backed by ObservableCollections.
I'm implementing a "Delete Item" context menu item for each ListView. Currently I have the SelectedItem for each ListView bound to the same object in my ViewModel. However to delete the item from the ObservableCollection requires the name of the ListView (in this case Wk01CECollection):
private void DeleteLO()
{
this.Wk01CECollection.Remove(SelectedCE);
}
Is there a way to reference the ListView that the SelectedItem is a member of? As it is I'll need to wire up a separate delete method for each ListView.
I'm sure this is not the "best practice" way to achieve this, but it's quite simple:
You could pass the ListView's selected item as a parameter to the command:
<Button Content="Delete selected item" Command="{Binding DeleteCommand}" CommandParamenter="{Binding ElementName=SomeListView, Path=SelecteItem}" />
Since you can try to remove an item from an ObservableCollection even if it does not exist in the collection without getting an exception, in your private Delete method you can try to remove that item from all the ObservableCollections you have in your ViewModel. I'm assuming here that an item cannot be in two collections at the same time, if this is not the case, what I said won't work because you'll remove the item from all your collections. If you plan to do this, just check for null before removing, if you try to remove a null object you'll get an exception.
private void DeleteLO(object listitem)
{
if(listitem != null)
{
if(listitem as CollectionType1 != null) //cast your listitem to the appropiate type inside your collection
this.Wk01CECollection.Remove(listitem);
if(listitem as CollectionType2 != null)
this.Wk02CECollection.Remove(listitem);
//etc.
}
}
Besides all of this, if you are using MVVM it is best that the ViewModel does not know about the View, so referencing the ListView inside the VM would break that principle.
I understand the issue is long gone, but will post for googlers.
This worked for me:
private void LvPrevKeyDown(object sender, KeyEventArgs e)
{
//Nothing to do here
if (Lv.SelectedItems.Count == 0) return;
//Empty space for other key combinations
//Let's remove items. If it's a simple delete key so we'll remove just the selected items.
if (e.Key != Key.Delete || Keyboard.Modifiers != ModifierKeys.None) return;
var tmp = Lv.SelectedItems.Cast<ColorItem>().ToList();
foreach (var colorItem in tmp)
{
_cList.Remove(colorItem);
}
}
Nothing fancy in the xaml. Just some columns bound to _cList properties and items source bound to _cList, of course.
Hope it'll help someone!
Kind regards!

How to add bindings for WindowsFormHost hosted ReportViewer control

I'm using Microsoft's ReportViewer control in my WPF application. Since this is a WinForms component, I use the WindowsFormHost control.
I try to follow the MVVM pattern as supported by the WPF Application Framework, so I implemented a ReportViewModel which contains (amongst others) the current report name and the dataset (both can be selected by 'regular' WPF controls, that part works fine).
I'd like to be as "WPF-ish" as possible, so how would I properly set up the binding to the ReportViewer component (which is inside the WindowsFormHost control)? I need to set the ReportViewer.LocalReport.ReportEmbeddedResource property and have a call to ReportViewer.LocalReport.DataSources.Add (and possibly Clear) whenever the view models report name or dataset change. What's the proper way to do that?
Is there any chance to use one of the regular WPF binding mechanisms for that? If yes, how? If no, how would I set up the binding? (its my first 'real' WPF project, so don't be shy to post trivial solutions :) ...)
Thanks!
So far I've come up with the following solution (purly code-behind):
private void MyDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
if (e.OldValue is ReportViewModel) {
var viewModel = e.OldValue as ReportViewModel;
viewModel.PropertyChanged -= ViewModelPropertyChanged;
}
if (DataContext is ReportViewModel) {
var viewModel = DataContext as ReportViewModel;
viewModel.PropertyChanged += ViewModelPropertyChanged;
SetReportData();
}
}
void ViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
if (e.PropertyName == "ReportName" || e.PropertyName == "ReportData")
SetReportData();
}
private void SetReportData() {
var viewModel = DataContext as ReportViewModel;
if (viewModel != null) {
var reportView = reportHost.Child as ReportViewer;
reportView.LocalReport.ReportEmbeddedResource = viewModel.ReportName;
reportView.LocalReport.DataSources.Clear();
reportView.LocalReport.DataSources.Add(new ReportDataSource("DataSet1", viewModel.ReportData as DataTable));
reportView.RefreshReport();
}
}
I'm still curious if there are any better solutions (I'm sure there are ...).

WPF - fill with data control, that is inside of another control

i have written some code, which fills controls with some data from a database. Everything works fine if controls are put directly in a window. But how to fill controls, which are inside other controls, such as TabControls, GroupBoxes etc.
My code looks like this:
Some window:
private void LoadDataP()
{
if (ID.Length > 0)
{
if (baseButtons.LoadProcedureSelectName != string.Empty)
LoadData = SqlHelper.GetTable(baseButtons.LoadProcedureSelectName, new string[] { IdName, ID });
if (LoadData != null)
foreach (DataRow dr in LoadData.Rows)
{
SqlHelper.FillWindowControllsWithData(myGrid, dr);
}
}
}
There are methods in another class.They do the main job:
public static void FillWindowControllsWithData(Grid windowGrid, DataRow dataRow)
{
foreach (Control ctrl in windowGrid.Children)
{
FillWindowControllsWithData(ctrl, dataRow);
}
}
public static void FillWindowControllsWithData(Control ctrl, DataRow dataRow)
{
if (ctrl.Name.IndexOf("db_") == 0)
{
if (ctrl is TextBox)
{
if (dataRow.Table.Columns.Contains(ctrl.Name.Substring(3)))
{
((TextBox)ctrl).Text = dataRow[ctrl.Name.Substring(3)].ToString();
}
}
} //end if
}
So does anybody know how to fill data in a texbox, which is in some groupbox or tabcontrol, which also have children..?
Just to amplify on what Dabbleml says: Writing WPF applications using Windows Forms techniques is a road to despair and misery. The happy, joyous, and free way to build WPF applications is through the MVVM pattern and data-binding.
The buzzwords make this sound a lot harder than it actually is. In practice it's conceptually quite simple:
Design your WPF window. This is your view.
Build a class that has exposes each piece of data that your window will display as a property. This is your view model. Add a method to the class that populates it from the data source. (This, by the way, is your model. Now you can say that you're using the MVVM pattern and impress girls at parties.)
Create a data context in your WPF window that will hold an instance of this class, and bind the controls to the class's properties.
What happens when you do this: the actual code you write all involves manipulating data, not the UI. When someone comes to you and asks if your window can display some kind of dancing bologna when some weird condition occurs, you don't write any UI code; you just add a property to your view model, and bind the dancing-bologna control in your view to it.
I think you could write the FillWindowControllsWithData method recursively. You need to alter the signature of the method though:
public void FillWindowControllsWithData(FrameworkElement element, DataRow dataRow)
{
if (element is TextBox)
{
if (element.Name.StartsWith("db_")
{
if (dataRow.Table.Columns.Contains(element.Name.Substring(3)))
{
((TextBox)element).Text = dataRow[element.Name.Substring(3)].ToString();
}
}
}
else if(element is Panel)
{
foreach(FrameworkElement el in ((Panel)element).Children)
{
FillWindowControllsWithData(el, dataRow);
}
}
else if(element is ContentControl)
{
FillWindowControllsWithData(((ContentControl)element).Content, dataRow);
}
//else do nothing
}
However,this definitely is not the way to do it. You need to fill your controls through databinding. Start here to make a start. Good Luck!

ListView Binding refresh suggestion in WPF

I have an ObservableCollection bound to a ListBox and have a highlight mechanism set up with DataTriggers, when I had a simple set of highlighters (debug, warning, etc) I could simply enumerate the style with several data-triggers bound to the view model that exposes those options.
I have now upgraded the system to support multiple userdefined highlighters which expose themselves with IsHighlighted(xxx) methods (not properties).
How can I make the the ListView aware that the visual state (style's datatrigger) has changed? Is there a "refreshed" event I can fire and catch in a DataTrigger?
Update:
I have a DataTrigger mapped to an exposed property Active which simply returns a value of true, but despite that there is no update:
<DataTrigger Binding="{Binding Highlight.Active}"
Value="true">
<Setter Property="Background"
Value="{Binding Type, Converter={StaticResource typeToBackgroundConverter}}" />
<Setter Property="Foreground"
Value="{Binding Type, Converter={StaticResource typeToForegroundConverter}}" />
</DataTrigger>
When the condition of a DataTrigger changes, this should automatically cause the parent UI element to refresh.
A couple of things to check:
1. The input data of the trigger is actually changing as you expect it to.
2. The input data of the trigger binds to a dependency property. Otherwise, you will never know when the value updates.
If you showed us the appropiate parts of your XAML, that would help a great deal.
If you just want to set the colour of the item somehow, you could write a converter that does what you want:
<Thing Background="{Binding Converter={StaticResource MyItemColorConverter}}" />
In this case, the converter could call your IsHighlighted(xxx) method and return the appropriate colour for the Thing.
If you want to set more than one property, you could use multiple converters, but the idea starts to fall apart at some point.
Alternatively, you could use a converter on your DataBinding to determine whether the item in question falls into a certain category and then apply setters. It depends upon what you need!
EDIT
I have just re-read your question and realised I'm off the mark. Whoops.
I believe you can just raise INotifyPropertyChanged.PropertyChanged with a PropertyChangedEventArgs that uses string.Empty, and that forces the WPF binding infrastructure to refresh all bindings. Have you tried that?
I'm going to answer my own question with an explanation of what I needed to do.
It's a long answer as it seems I kept hitting against areas where WPF thought it knew better and would cache. If DataTrigger had a unconditional change, I wouldn't need any of this!
Firstly, let me recap some of the problem again. I have a ListView that can highlight different rows with different styles. Initially, these styles were built-in types, such as Debug and Error. In these cases I could easily latch onto the ViewModel changes of them as DataTriggers in the row-style and make each update immediately.
Once I upgraded to allow user-defined highlighters, I no longer had a property to latch onto (even if I dynamically created them, the style wouldn't know about them).
To get around this, I have implemented a HighlightingService (this can be discovered at any point by using my ServiceLocator and asking for a IHightlightingServce supporting instance). This service implements a number of important properties and methods:
public ObservableCollection<IHighlighter> Highlighters { get; private set; }
public IHighlighterStyle IsHighlighted(ILogEntry logEntry)
{
foreach (IHighlighter highlighter in Highlighters)
{
if ( highlighter.IsMatch(logEntry) )
{
return highlighter.Style;
}
}
return null;
}
Because the Highlighters collection is publicly accessible, I decided to permit that users of that collection could add/remove entries, negating my need to implement Add/Remove methods. However, because I need to know if the internal IHighlighter records have changed, in the constructor of the service, I register an observer to its CollectionChanged property and react to the add/remove items by registering another callback, this allows me to fire a service specific INotifyCollectionChanged event.
[...]
// Register self as an observer of the collection.
Highlighters.CollectionChanged += HighlightersCollectionChanged;
}
private void HighlightersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newItem in e.NewItems)
{
System.Diagnostics.Debug.Assert(newItem != null);
System.Diagnostics.Debug.Assert(newItem is IHighlighter);
if (e.NewItems != null
&& newItem is IHighlighter
&& newItem is INotifyPropertyChanged)
{
// Register on OnPropertyChanged.
IHighlighter highlighter = newItem as IHighlighter;
Trace.WriteLine(string.Format(
"FilterService detected {0} added to collection and binding to its PropertyChanged event",
highlighter.Name));
(newItem as INotifyPropertyChanged).PropertyChanged += CustomHighlighterPropertyChanged;
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldItem in e.OldItems)
{
System.Diagnostics.Debug.Assert(oldItem != null);
System.Diagnostics.Debug.Assert(oldItem is IHighlighter);
if (e.NewItems != null
&& oldItem is IHighlighter
&& oldItem is INotifyPropertyChanged)
{
// Unregister on OnPropertyChanged.
IHighlighter highlighter = oldItem as IHighlighter;
Trace.WriteLine(string.Format(
"HighlightingService detected {0} removed from collection and unbinding from its PropertyChanged event",
highlighter.Name));
(oldItem as INotifyPropertyChanged).PropertyChanged -= CustomHighlighterPropertyChanged;
}
}
}
}
private void CustomHighlighterPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if ( sender is IHighlighter )
{
IHighlighter filter = (sender as IHighlighter);
Trace.WriteLine(string.Format("FilterServer saw some activity on {0} (IsEnabled = {1})",
filter.Name, filter.Enabled));
}
OnPropertyChanged(string.Empty);
}
With all of that, I now know whenever a user has changed a registered highlighter, but it has not fixed the fact that I can't associate a trigger to anything, so I can reflect the changes in the displayed style.
I couldn't find a Xaml only way of sorting this, so I made a custom-control containing my ListView:
public partial class LogMessagesControl : UserControl
{
private IHighlightingService highlight { get; set; }
public LogMessagesControl()
{
InitializeComponent();
highlight = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlight != null && highlight is INotifyPropertyChanged)
{
(highlight as INotifyPropertyChanged).PropertyChanged += (s, e) => UpdateStyles();
}
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
private void UpdateStyles()
{
messages.ItemContainerStyleSelector = null;
messages.ItemContainerStyleSelector = new HighlightingSelector();
}
}
This does a couple of things:
It assigns a new HighlightingSelector to the ItemContainerStyleSelector (the ListView is called messages).
It also registers itself to the PropertyChanged event of the HighlighterService which is a ViewModel.
Upon detecting a change, it replaces the current instance of HighlightingSelector on the ItemContainerStyleSelector (note, it swaps to null first as there is a comment on the web attributed to Bea Costa that this is necessary).
So, now all I need is a HighlightingSelector which takes into account the current highlighting selections (I know that should they change, it will be rebuilt), so I don't need to worry about things too much). The HighlightingSelector iterates over the registered highlighters and (if they're enabled) registers a style. I cache this in a Dictionary as rebuilding these could be expensive and since they only get built at the point the user has made a manual interaction, the increased cost of doing this up front isn't noticeable.
The runtime will make a call to HighlightingSelector.SelectStyle passing in the record I care about, all I do is return the appropriate style (which was based upon the users original highlighting preferences).
public class HighlightingSelector : StyleSelector
{
private readonly Dictionary<IHighlighter, Style> styles = new Dictionary<IHighlighter, Style>();
public HighlightingSelector()
{
IHighlightingService highlightingService = ServiceLocator.Instance.Get<IHighlightingService>();
if (highlightingService == null) return;
foreach (IHighlighter highlighter in highlightingService.Highlighters)
{
if (highlighter is TypeHighlighter)
{
// No need to create a style if not enabled, should the status of a highlighter
// change, then this collection will be rebuilt.
if (highlighter.Enabled)
{
Style style = new Style(typeof (ListViewItem));
DataTrigger trigger = new DataTrigger();
trigger.Binding = new Binding("Type");
trigger.Value = (highlighter as TypeHighlighter).TypeMatch;
if (highlighter.Style != null)
{
if (highlighter.Style.Background != null)
{
trigger.Setters.Add(new Setter(Control.BackgroundProperty,
new SolidColorBrush((Color) highlighter.Style.Background)));
}
if (highlighter.Style.Foreground != null)
{
trigger.Setters.Add(new Setter(Control.ForegroundProperty,
new SolidColorBrush((Color) highlighter.Style.Foreground)));
}
}
style.Triggers.Add(trigger);
styles[highlighter] = style;
}
}
}
}
public override Style SelectStyle(object item, DependencyObject container)
{
ILogEntry entry = item as ILogEntry;
if (entry != null)
{
foreach (KeyValuePair<IHighlighter, Style> pair in styles)
{
if (pair.Key.IsMatch(entry) && pair.Key.Enabled)
{
return pair.Value;
}
}
}
return base.SelectStyle(item, container);
}
}

Resources