-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refresh clears ListBox selection #9
Comments
Selection is a genuine pain. I have often resorted to a little custom code around the bind to keep items selected. I can't remember exactly what I did however as it's been about 5 years since I did any UI work |
So, your goal for the Can you provide any code samples/snippets for what you have now? |
I'm glad it's not just something I've done wrong, but I hope you can remember what it was you did, because I'm not sure how I can prevent removals when they could be genuine deselections.
The search text doesn't (shouldn't) change which items are selected, but items which are already selected won't be deselected by the search filter. Basically the search filter is: I can't provide the exact code unfortunately. |
Well, I can demonstrate what I would do to achieve this... public record MyListItem
{
public required int Id { get; init; }
public required string Text { get; init; }
}
public class MyListItemModel
: INotifyPropertyChanged
{
public static MyListItemModel Create(
int itemId,
IObservableCache<MyListItem, int> itemsSource);
public int Id { get; }
public string Text { get; private set; }
public bool IsSelected { get; set; }
}
...
using var itemsSource = new SourceCache<MyListItem, int>(static item => item.Id);
using var searchText = new BehaviorSubject<string>(string.Empty);
var itemModelsCache = = itemsSource
.Connect()
.DistinctValues(static item => item.Id)
.Transform(itemId => MyItemViewModel.Create(itemId, itemsSource))
.AsObservableCache();
using var itemModelsSubscription = itemModelsCache
.Connect()
.Sort(...)
.Bind(out var itemModels)
.Subscribe();
var filteredItems = itemModeslCache
.Connect()
.Filter(searchText
.DistinctUntilChanged()
.Select(static searchText => new Func<MyListItemModel, bool>(itemModel
=> itemModel.Matches(searchText) || itemModel.IsSelected))); |
Yeah, I had been thinking about wrapping my items in a class Your example is adding IsSelected to the same VM object but seems like it would amount to the same thing. I'm just not sure what I think I might need to look at the reference source to see how UPDATE: I figured I would listen to the collectionchanged events on my bound r/o collection and each time it refreshes there are a bunch of I'm going to try filtering out those change events with a wrapper ObservableCollection to see what happens. |
So wrapping my bound public class ReadOnlyObservableCollectionNoReplaceSameObject<T> : IReadOnlyList<T>, INotifyCollectionChanged
{
private readonly IReadOnlyList<T> _items;
private readonly INotifyCollectionChanged _ncc;
private readonly Dictionary<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventHandler> _filterLookup = new();
public ObservableCollectionNoReplace(ReadOnlyObservableCollection<T> collection)
{
_items = collection;
_ncc = collection;
}
/// <inheritdoc />
public int Count => _items.Count;
/// <inheritdoc />
public T this[int index] => _items[index];
/// <inheritdoc />
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_items).GetEnumerator();
/// <inheritdoc />
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add => _ncc.CollectionChanged += FilterOutReplace(value);
remove => _ncc.CollectionChanged -= GetFilter(value);
}
private NotifyCollectionChangedEventHandler GetFilter(NotifyCollectionChangedEventHandler value)
{
return _filterLookup.TryGetValue(value, out var handler) ? handler : null;
}
private NotifyCollectionChangedEventHandler FilterOutReplace(NotifyCollectionChangedEventHandler value)
{
NotifyCollectionChangedEventHandler handler = (o, e) =>
{
if (e.Action != NotifyCollectionChangedAction.Replace || e.NewItems[0] != e.OldItems[0])
{
value(o, e);
}
};
_filterLookup.Add(value, handler);
return handler;
}
} Could you add an option to the I do have one remaining issue, which is that |
To me, this doesn't sound so much like a problem as "the normal work you have to do to build a View layer". I'm also not sure what you mean by "down-select" here. Even if the VM is structured like you describe, as a <ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding Data}"/>
<DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
So, you're saying that your source stream is issuing updates instead of refreshes, when the item hasn't actually changed, only its state has? That there would be your core problem, stemming from something you're not doing properly in the creation of these objects, or in your composition of operators, or from a bug in DD. Have a peak at what the changeset stream looks like going INTO the binding. From what you describe of your scenario, you should only be seeing
This is a convincing proof of what the problem is, but it's not here, it's upstream. See above.
Wouldn't have any practical use, as this kinda thing shouldn't happen in the first place. See above.
You say the second one doesn't trigger a Also, I HIGHLY recommend against doing either of these, it's going to murder your performance. This will cause a separate changeset, each with a single What is it that you're trying to achieve with these two? Filtering? If so, change-detection for filtering criteria should be handled within the items
.Filter(
predicateChanged: Observable.Return<Func<MyListItem, bool>>(
item => item.Name.Matches(SearchText)
|| SelectedItems.Contains(item)),
reapplyFilter: Observable.Merge(
this.WhenValueChanged(x => x.SearchText)
.Select(static _ => Unit.Default),
SelectedItems
.ToObservable()
.Select(static _ => Unit.Default)); (This is much more verbose than it should be, BTW, due to lack of enough variety in overloads for this operator. That's on my to-do list). |
Yes, that's basically what I meant, and no it's not that complicated, but it's annoying to have to add that to a view which should handle selection without it to solve a bug. I think it's a reasonable way to handling selection if you don't want to make SelectedItems bindable.
I wasn't sure how to peek at what's going on inside the pipe, do I just make a new extension method and observer/observee that does nothing to put in the chain and put some breakpoints there, or is there some way to inspect them in the DD API already?
I'm quite happy to do this entirely differently as my understanding was that it was refreshing the entire pipeline just to refresh the filter and that seemed overkill. In another VM I tried to create an observable filter from multiple owner VM properties changing using: I couldn't observe the selection collection changing in the property changed ones anyway, so it seemed a better fit, but the collection isn't triggering a refresh. However, I can't find the overload you're talking about, did you mean this? .Filter(Observable.Merge(
this.WhenPropertyChanged(x => x.SearchText).Select(_ => Unit.Default),
SelectedItems.ToObservable().Select(_ => Unit.Default))
.Select(_ => (Func<T, bool>)DoFilter)) I tried it and it avoids the unwanted downsteam Now I understand it better and it works, I would recommend two additonal extension methods
|
I'm just talking about looking at the changesets passing between operators. .Do(changeSet => { /* breakpoint here */ })
Your version doesn't look like it would be meaningfully different. It makes more sense to read, but it also generates a new redundant The overload I was referring to is... public static IObservable<IChangeSet<TObject, TKey>> Filter<TObject, TKey>(
this IObservable<IChangeSet<TObject, TKey>> source,
IObservable<Func<TObject, bool>> predicateChanged,
IObservable<Unit> reapplyFilter,
bool suppressEmptyChangeSets = true)
where TObject : notnull
where TKey : notnull
That overload is basically what I was looking for, and was kinda surprised it doesn't exist. Although, I definitely wouldn't put the However, there's also a new I'm not quite sure if what you've got now is the most effective solution, but I'm glad you're working regardless. Let us know if you need any further advice. |
Hi, I'm trying to decouple my VMs from WPF libraries and DynamicData seems to be a great way to do that. I've managed to figure out most of what I was doing before using your snippets and examples, but I've hit something I don't know how to solve.
I had two observable collections, one with all the items and another with the items selected in that collection (bound using an attached behaviour on ListView, which worked fine before). I also have a search text filter for these items, but I want to keep the selected items in the list even if they don't match the search text, so my filter method takes both of those things into account.
I have used
.AutoRefreshOnObservable(_ => ...)
to listen to changes to the search text usingthis.WhenValueChanged(...)
and the selected items observable using.ToObservable()
, which is updating the filter correctly, but is also deselecting all my items when it refreshes.I found two stackoverflow questions which seemed relevant to my issue:
resetThreshold: int.MaxValue
didn't work for me.I'd really like to be able to continue using this way of presenting my data to the View, but I can't if the collection clearing on changes is unavoidable, so any assistance would be appreciated, thanks.
The text was updated successfully, but these errors were encountered: