I'm working on a WPF MVVM application and running into an issue. I'm familiar with WPF itself, however I've rarely used MVVM and I suspect I am doing something that MVVM doesn't support, however I don't know how else to accomplish what I am trying to do.
In the application, I have a user control called Agenda. It consist several controls including a text box, a button to add a new agenda item, and a list box with a custom template. The template includes an expander where the header is the agenda item title, up/down arrows to reorder items, and a button to delete the item. The expander content contains a toolbar and a rich text box. In the agenda UC I have a dependency property called ItemsSource which is an IEnumerable<AgendaItem>.
Now, I have a view called Appointment, its associated VM (AppointmentViewModel), and its model (AppointmentModel). In the model, there is a field called AgendaItems which is an ObservableCollection<AgendaItem>. The agenda UC is used within the appointment view and the UC's ItemsSource is bound to the Model.AgendaItems (the observable collection).
The problem I'm having is when I try to handle the buttons to reorder the agenda items in the UC. As an example, for the button to move an agenda item up the list, this is the code in the UC:
var tb = sender as Button;
var tag = tb.Tag as AgendaItem;
var lst = ItemsSource.ToList();
var index = lst.IndexOf(tag);
if(index > 0)
{
lst.RemoveAt(index);
lst.Insert(index - 1, tag);
ItemsSource = lst;
}
The tag of the up arrow is bound to the specific agenda item in the list so I know which item is being moved. The problem comes after I update the ItemsSource property doing ItemsSource = lst. After that line executes, the AgendaItems ObservableCollection in the VM is null. The binding mode is set to TwoWay.
Since the the appointment UC is used in various windows in the application, it made sense to me that the reordering of agenda items should be taken care of my the UC instead of duplicating code in every window which makes use of the UC. But updating the ItemsSource property in the UC results in the collection in the VM being null.
For reference, the ItemsSource property in the UC is defined as:
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
There is the regular .NET property:
public IEnumerable<AgendaItem> ItemsSource
{
get => (IEnumerable<AgendaItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
And the OnItemsSourceChanged method is:
private static void OnItemsSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is Agenda control)
{
if (e.OldValue is INotifyCollectionChanged oldValueINotifyCollectionChanged)
{
oldValueINotifyCollectionChanged.CollectionChanged -= control.ItemsSource_CollectionChanged;
}
if (e.NewValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
{
newValueINotifyCollectionChanged.CollectionChanged += control.ItemsSource_CollectionChanged;
}
}
}
Any help/guidance on how I can reorder the ItemsSource collection in the UC without breaking the VM would be greatly appreciated. Thank you in advance.
Since calling .ToList() on Enumerable creates new list and reassigning it to ItemsSource doesn't work, maybe define the ItemsSourceProperty as IList<AgendaItem> since this is the interface that you need to reorder the items.
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IList<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
(don't forget to change the signature of the ItemsSource property ).
You could also create list extension method to call it in your code like this:
ItemsSource.Move(ItemsSource.IndexOf(tag), MoveDirection.Up);
public static void Move<T>(this IList<T> list, int iIndexToMove,
MoveDirection direction)
{
if (list.Count > 0 && (direction == MoveDirection.Down && iIndexToMove < list.Count - 1)
|| (direction == MoveDirection.Up && iIndexToMove > 0))
{
if (direction == MoveDirection.Up)
{
var old = list[iIndexToMove - 1];
list[iIndexToMove - 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
else
{
var old = list[iIndexToMove + 1];
list[iIndexToMove + 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
}
}
public enum MoveDirection
{
Up,
Down
}
By changing the data type of ItemsSource from IEnumerable<AgendaItem> to ObservableCollection<AgendaItem> resolved the issue. Thank you to everyone who responded. It is much appreciated.
Related
I've got a ViewModel, which is used by a View with a DataGrid.
In the DataGrid I try to allow the User directly to add new rows, but some of the columns need to open a new Dialog for the value.
Therefor I set the SelectedItem - Binding and added some Extensions to allow binding OnBeginningEdit and OnRowEditEnding to custom commands in my ViewModel.
Now I have the problem, that I want to get the current "new" item, which is partly filled by the GUI, before calling CommitEdit on the DataGrid.
I tried to use IEditableCollectionView and use CurrentAddItem of it, to achieve this.
But the CurrentAddItem does not get filled with the items entered in the GUI.
How may I achieve that properly in MVVM?
public ICommand DataGridDoubleClickCommand
{
get
{
return new Command(
delegate (object obj)
{
var dataGrid = ((object[])obj)[0] as DataGrid;
var args = ((object[])obj)[1] as MouseButtonEventArgs;
if (dataGrid.CurrentColumn.DisplayIndex != 2)
{
return;
}
args.Handled = true;
ICollectionView view = CollectionViewSource.GetDefaultView(dataGrid.Items);
IEditableCollectionView iecv = (IEditableCollectionView)view;
if(iecv.IsAddingNew)
{
// tried to use iecv.CurrentAddItem here - but was empty
}
dataGrid.CommitEdit();
...
Could someone help me, how can I update ObservableCollection, which is binded to ListView ItemSource, without blinking? When I do:
Contacs = _contacs;
the whole ListView is blinking. I would like to search in ListView too, but always after replacing the old results with new one, the listview blinks.
The problem here is, that you are reassigning the whole collection. This does not take advantage of the observability and forces the ListView to reload all items. Try to remove/add the items instead so the ListView only needs to update the Items that actually changed.
In the case of searching hiding the unmatched results might be a viable solution too. To do that create a property on your Contact type (called "IsVisible" for example) and bind it to the ListViewItems Visibility Property. (You might need the build in BooleanToVisibility Converter here)
Update
As pointed out in the comments using a CollectionViewSource is the correct wpf way of implementing a search filter. See this answer for details on how to use it.
If you want to enable filtering in your collection then there is no actual need to perform operations directly on your collection.
Use ICollectionView and CollectionViewSource for this purpose.
As you have an ObservableCollection so you can do something like this.
ICollectionView contactsView;
public ICollectionView ContactsView
{
get { return contactsView; }
set
{
if(contactsView != value)
{
contactsView = value;
}
}
}
And in the setter of the ObservableCollection
public ObservableCollection<ContactType> Contacs
{
get { return _contacs; }
set
{
if(_contacs != value)
{
_contacs = value;
ContactsView = CollectionViewSource.GetDefaultView(value);
ContactsView.Filter = ContactsFilter;
}
}
}
where ContactsFilter is a function with following definition.
bool ContactsFilter(object item)
{
var contact = item as ContactType;
if(condition)
return true; //show this item in ListView.
return false; //Do not show this item in ListView
}
and whenever you want to filter items you can do that just by
ContactsView.Refresh();
which I think will be in the TextChanged Event of your text box in which you are entering search query.
More detailed article is at CollectionViewSource
Some items in the ListView control will be selectable and have normal text.
Some items however, although included in the ListView as items, will be unselectable/unclickable and 'greyed-out'.
In Windows-Store-Apps we have the ability to select Single/Multiple/None items in a ListView. But how can make certain items at certain indexes unselectable/unclickable and 'greyed-out', in code mainly?
I managed to access the Item of the ListView at a certain index:
myListView.ItemContainerGenerator.ContainerFromIndex(i)
But I couldn't find any option to customize its selected event handler.
Any idea how to achieve that?
In Single selection mode.
First Add a boolean property to class of binding type which defines which items are clickable like this
class TestClass
{
Boolean IsClickAllowed{get;set;}
string name{get;set;}
}
then create a source list of TestClass type and set it as itemssource of Listview like this
var TempList=new List<>()
{
new TextClass(){IsClickAllowed=false,name="First Item"},
new TextClass(){IsClickAllowed=true,name="Second Item"},
new TextClass(){IsClickAllowed=false,name="Third Item"},
};
MyList.ItemsSource=TempList;
and for greying out Set Different DataTemplate for nonClickable items implementing DataTemplateSelector and finally for click handle in ItemClick event. You need to set IsItemClickEnabled as true.
private void MyList_ItemClick(object sender, ItemClickEventArgs e)
{
var item = e.ClickedItem as TestClass;
if (item != null){
if(item.IsClickAllowed){
//Do Stuff here
}else
{
//Do Nothing
}
}}
Hope it helps.
I have found a solution:
I have override the ListView control and create a StripedListView. Then by overriding the PrepareContainerForItemOverride, which is responsible for the setting up the ListViewItem control after it’s be created, you could modify the background color and set the ItemListView.isEnabled option to false:
public class StripedListView : ListView
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var listViewItem = element as ListViewItem;
if (listViewItem != null)
{
var index = IndexFromContainer(element);
if (Words.arrayW[index].Length > 0)
{
listViewItem.Foreground = new SolidColorBrush(Colors.Black);
}
else
{
listViewItem.Foreground = new SolidColorBrush(Colors.Gray);
listViewItem.IsEnabled = false;
}
}
}
}
In Xaml:
<controls:StripedListView x:Name="letterListView" ItemsSource="{Binding}">
<controls:StripedListView.ItemTemplate>
<DataTemplate>
etc...
</DataTemplate>
</controls:StripedListView.ItemTemplate>
</controls:StripedListView>
What I'm trying to do :
I have 2 comboboxes, a "regular" one, and a filtrable one. On the filtrable combobox ItemsSource is binded to a property of the first combobox SelectedItem.
Here is some XAML to demonstrate the relation (I removed some properties):
<ComboBox Name="cbx_poche" ItemsSource="{Binding DataContext.ListePoches, ElementName=Main}" SelectedItem="{Binding PocheCible}" />
<controls:FilteredComboBox ItemsSource="{Binding SelectedItem.SupportsEligibles, ElementName=cbx_poche}" SelectedItem="{Binding Support}" />
The FilteredComboBox is a derived class from ComboBox inspired by those articles : Building a Filtered ComboBox for WPF / WPF auto-filtering combo.
The user can type in the combobox and it filters the list to display the items matching. The default behavior of the combobox is NOT desired (it automatically completes what the user types), this is why it's derived.
Those comboboxes above are in a ItemsControl element, because I need to have one row for each item in a specific collection. The SelectedItem properties of the comboboxes are binded to the item in this collection.
The result :
The problem :
It works quite well... as long as you don't select the same item in the first combobox (like in the example above : if I type some text that doesn't match the above combo, it will be reset).
As soon as several FilteredComboBox are linked to the same item in the first combobox (so binded to SelectedItem.SupportsEligibles), typing text in the FilteredComboBox filters both lists.
I know why it does that, I don't know how to fix it. So I tried two things :
Code 1 (current code) :
The problem is that the code uses the default view on the list, so all controls binded to this list will apply the same filter :
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
if (ItemsSourceView != null)
ItemsSourceView.Filter -= this.FilterPredicate;
ItemsSourceView = CollectionViewSource.GetDefaultView(newValue);
ItemsSourceView.Filter += this.FilterPredicate;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
Code 2 :
So my (naïve) idea was to get a local view from the binded view. I works well for filtering, but breaks the binding (changing the selected item in the first combo doesn't update the list after the first pass)
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null && newValue != ItemsSourceView)
{
if (ItemsSourceView != null)
ItemsSourceView.Filter -= this.FilterPredicate;
ItemsCollectionViewSource = new CollectionViewSource { Source = newValue };
ItemsSourceView = ItemsCollectionViewSource.View;
ItemsSourceView.Filter += this.FilterPredicate;
this.ItemsSource = ItemsSourceView; // Breaks the binding !!
}
base.OnItemsSourceChanged(oldValue, newValue);
}
I'm stuck here.
I'm looking for some event or Binding class that I could use to be notified of the binding change, in order to update the view. Or maybe getting the view applied without having to change the ItemsSource
I ended up using a quite lame workaround, so I'm still interested in smart answers.
For people interested : I added another similar ItemsSource2 Dependency Property (still didn't find a nice name for it) on which I bind my item list, instead of the original ItemsSource.
When this items source is changed, the control gets a new CollectionView (not the default one) and sets it to the "standard" ItemsSource.
The other elements of the control remains identical (similar to the code in the linked articles).
public static readonly DependencyProperty ItemsSource2Property =
DependencyProperty.Register(
"ItemsSource2",
typeof(IEnumerable),
typeof(FilteredComboBox),
new UIPropertyMetadata((IEnumerable)null, new PropertyChangedCallback(OnItemsSource2Changed)));
[Bindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IEnumerable ItemsSource2
{
get { return (IEnumerable)GetValue(ItemsSource2Property); }
set
{
if (value == null)
{
ClearValue(ItemsSource2Property);
}
else
{
SetValue(ItemsSource2Property, value);
}
}
}
private static void OnItemsSource2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ic = (FilteredComboBox)d;
var oldValue = (IEnumerable)e.OldValue;
var newValue = (IEnumerable)e.NewValue;
if (newValue != null)
{
//Prevents the control to select the first item automatically
ic.IsSynchronizedWithCurrentItem = false;
var viewSource = new CollectionViewSource { Source = newValue };
ic.ItemsSource = viewSource.View;
}
else
{
ic.ItemsSource = null;
}
}
I am using a ListView control to display some lines of data. There is a background task which receives external updates to the content of the list. The newly received data may contain less, more or the same number of items and also the items itself may have changed.
The ListView.ItemsSource is bound to an OberservableCollection (_itemList) so that changes to _itemList should be visible also in the ListView.
_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;
In order to avoid refreshing the complete ListView I do a simple comparison of the newly retrieved list with the current _itemList, change items which are not the same and add/remove items if necessary. The collection "newList" contains newly created objects, so replacing an item in _itemList is correctly sending a "Refresh" notification (which I can log by using the event handler OnCollectionChanged of the ObservableCollection`)
Action action = () =>
{
for (int i = 0; i < newList.Count; i++)
{
// item exists in old list -> replace if changed
if (i < _itemList.Count)
{
if (!_itemList[i].SameDataAs(newList[i]))
_itemList[i] = newList[i];
}
// new list contains more items -> add items
else
_itemList.Add(newList[i]);
}
// new list contains less items -> remove items
for (int i = _itemList.Count - 1; i >= newList.Count; i--)
_itemList.RemoveAt(i);
};
Dispatcher.BeginInvoke(DispatcherPriority.Background, action);
My problem is that if many items are changed in this loop, the ListView is NOT refreshing and the data on screen stay as they are...and this I don't understand.
Even a simpler version like this (exchanging ALL elements)
List<PmemCombItem> newList = new List<PmemCombItem>();
foreach (PmemViewItem comb in combList)
newList.Add(new PmemCombItem(comb));
if (_itemList.Count == newList.Count)
for (int i = 0; i < newList.Count; i++)
_itemList[i] = newList[i];
else
{
_itemList.Clear();
foreach (PmemCombItem item in newList)
_itemList.Add(item);
}
is not working properly
Any clue on this?
UPDATE
If I call the following code manually after updating all elements, everything works fine
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
But of course this causes the UI to update everything which I still want to avoid.
After a change, you can use the following to refresh the Listview, it's more easy
listView.Items.Refresh();
This is what I had to do to get it to work.
MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;
I know that's an old question, but I just stumbled upon this issue. I didn't really want to use the null assignation trick or the refresh for just a field that was updated.
So, after looking at MSDN, I found this article:
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2
To summarize, you just need the item to implement this interface and it will automatically detect that this object can be observed.
public class MyItem : INotifyPropertyChanged
{
private string status;
public string Status
{
get => status;
set
{
OnPropertyChanged(nameof(Status));
status = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
So, the event will be called everytime someone changes the Status. And, in your case, the listview will add a handler automatically on the PropertyChanged event.
This doesn't really handle the issue in your case (add/remove).
But for that, I would suggest that you have a look at BindingList<T>
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2
Using the same pattern, your listview will be updated properly without using any tricks.
You should not reset ItemsSource of ListView each time observable collection changed. Just set proper binding that will do your trick. In xaml:
<ListView ItemsSource='{Binding ItemsCollection}'
...
</ListView>
And in code-behind (suggest to use MVVM) property that will be responsible for holding _itemList:
public ObservableCollection<PmemCombItem> ItemsCollection
{
get
{
if (_itemList == null)
{
_itemList = new ObservableCollection<PmemCombItem>();
}
return _itemList;
}
}
UPDATE:
There is similar post which most probably will Answer your question: How do I update an ObservableCollection via a worker thread?
I found a way to do it. It is not really that great but it works.
YourList.ItemsSource = null;
// Update the List containing your elements (lets call it x)
YourList.ItemsSource = x;
this should refresh your ListView (it works for my UAP :) )
An alternative on Xopher's answer.
MyListView.ItemsSource = MyDataSource.ToList();
This refreshes the Listview because it's a other list.
Please check this answer:
Passing ListView Items to Commands using Prism Library
List view Items needs to notify about changes (done is setter)
public ObservableCollection<Model.Step> Steps
{
get { return _steps; }
set { SetProperty(ref _steps, value); }
}
and UpdateSourceTrigger need to be set in xaml
<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />