Closed joadoumie closed 1 week ago
This sounds like a combination of a couple different asks:
IAsyncAction<T>
vs just T
IAsync
across processes was Considered Harmful.ISection
of IListItems
. It was kinda hard to implement sections like this.ItemsChanged
as in the spec IIRC), so that definitely needs to be doneShould we separate some of this out?
Yeah this was the top level item that needed to be broken out -- seems like you've done that above, so I'll go ahead and create the additional items and link them here (there is already one for Shmueli).
Based on feedback from Hackathon and folks building extensions -- it'd be interesting if we could make things a bit better for devs by:
It was about using the newer C# IAsyncEnumerable
pattern to help synchronize between the extensions and the app to incrementally return results. But talking to Sergio, there's not equivalent WinRT, so that's probably a non-starter?
Probably naive question - is that something we can make better in WinRT if it's an important paradigm?
is that something we can make better in WinRT if it's an important paradigm?
Probably not, certainly not in the timelines we're thinking about.
FWIW I think the best notes I had on the topic are around here: https://github.com/zadjii-msft/PowerToys/blob/main/src/modules/cmdpal/doc/initial-sdk-spec/initial-sdk-spec.md#updating-the-list
[!WARNING] We chose this API surface for a few reasons:
IObservableCollection
, which has quite a good mechanism for specifying exactly which parts of the collection changed, isn't exposed through WinRTIObservableVector
doesn't work well across process boundaries- In general, all the collection WinRT objects are Considered Harmful in cross-proc scenarios
But we want both static and dynamic lists to be able to update the results in real-time. Example: A process list that updates in real-time. We want to be able to add and remove items from the list as they start and stop.
To mitigate how dumb all that is, the plan I came up with Hart was:
IDynamicListPage
, CmdPal will call IListPage.SearchText(string)
whenever the search text changes.
SearchText
are just no-opsItemsChanged
GetItems
on the extensionGetItems
with the items it stashed at [A]That way, the replies to GetItems
should remain quick
(this also doesn't look spec'd very well)
That however does not deal at all with anything "paginated" though. Like, the HN extension just returns the first 24 items, with no way to indicate "I could have more items for you if you want".
That ^^^^ I had mentally punted out of v0, but maybe we shouldn't
I don't know if it's projected in WinRT, but there is the ISupportIncrementalLoading
interface too.
I think that's the crux is how much does the UI pre-filter what it has vs. when it goes to the extension and what does that look like and how we minimize the creation of new/duplicate items:
Okay some feedback from the hackathon was that we didn't exactly support
extensions that wanted a list of items that changed over time. Additionally, the
IDynamicListPage.GetItems
was a little... bad. You had to reply to the chenge
in the query immediately in the GetItems
call (again, because there was no way
to change the list of items after you returned it).
To remedy, I'm proposing the following changes to the API:
+ interface INotifyItemsChanged {
+ event Windows.Foundation.TypedEventHandler<Object, ItemsChangedEventArgs> ItemsChanged;
+ }
+ runtimeclass ItemsChangedEventArgs {
+ Int32 TotalItems { get; };
+ }
-interface IListPage requires IPage {
+interface IListPage requires IPage, INotifyItemsChanged {
// DevPal will be responsible for filtering the list of items, unless the
// class implements IDynamicListPage
String SearchText { get; };
String PlaceholderText { get; };
Boolean ShowDetails{ get; };
IFilters Filters { get; };
IGridProperties GridProperties { get; };
+ Boolean HasMoreItems { get; };
IListItem[] GetItems();
+ void LoadMoreItems();
}
interface IDynamicListPage requires IListPage {
- IListItem[] GetItems(String query); // DevPal will do no filtering of these items
+ // DevPal will do no filtering of these items
+ void SearchText { set; };
}
To summarize:
ItemsChanged
event to listsSo, for something like a dynamic page (let's imagine, the GitHub issues search)
IDynamicListPage
, so the command palette knows not to do any
host-side filtering.SearchText
from the ListPage
is:issue is:open
as initial textHasMoreItems
from the ListPage
true
as initial textGetItems()
GetMore
on the ListPage, to let it know it should start
fetching more resultsItemsChanged(40)
, to indicate that it now has 40 items
GetItems
, which the command returns the whole list of items
PropChanged("HasMoreItems")
here,
but we'll skip that for simplicityfoo
.
page.SearchText("is:issue is:open foo")
, to let the
extension know the query has changedItemsChanged(5)
, to indicate there are 5 resultsGetItems
to fetch the items.So that kinda covers both Pagination and making async dynamic query updates
simpler. Or if you wanted to be simpler, you don't need to set HasMoreItems
.
Say you just had the PingExtension
, that just does a ping -t 8.8.8.8
. Every
second, it writes the most recent line of output to a new ListItem
. In that
case, the author can just raise an ItemsChanged
each second, and return the
updated list each time.
[^1]: HasMoreItems
/ LoadMoreItems
is almost exactly ISupportIncrementalLoading
, but without 1: the async aspect, 2: forcing us to pull in XAML as a dependency, and 3: having possibly branched inheritance trees, which x-proc winrt hates. We can still use ISupportIncrementalLoading
on our side, wrapping up a list that implements that.
@zadjii-msft looking like a good step forward. All for trying to own filtering more in the UI layer, makes basic scenario simpler for extension developers. Couple of comments/questions:
SearchText
like a method, but then the interface has a property? Did you mean it to be a function?void SearchText { set; };
vs.
page.SearchText("is:issue is:open foo")
We could have a method that extension can call to essentially clear out the current results? i.e. just wondering if we want to define the possible behaviors/scenarios between the app and the extension a bit separately vs. as one long example. That way we can understand if we have all the pieces we need where we're either drilling down to finer results or the extension needing to 'restart' to return a new list of items based on the changed query.
For the following remark on how we update the UI:
CmdPal does an in-place update of the existing items - ignoring the unchanged ones, and appending the new ones to the end of the list
If we want to support updating existing ones in-place, this seems to preclude that they'd be record
types for items and instead classes, eh? (Is there an interface here or do we just have a basic object that holds the raw data)? So, if it is something where you can update the text of an existing item, then I imagine we'll have some sort of Id
per item? Otherwise, how do we expect the UI to know which items returned correspond to the prior items it received? If they're re-serialized we won't have a direct reference to the instance to compare, right?
In your examples you've called out the SearchText like a method, but then the interface has a property? Did you mean it to be a function?
You know, that comes from my background usually writing C++WinRT, where properties are implemented as methods.
We could have a method that extension can call to essentially clear out the current results
I don't see why an extension couldn't just raise ItemsChanged
, then return []
in the subsequent call it'll get for GetItems
. Maybe I'm missing something on this scenario?
(I'll reply to 3 tomorrow)
Thanks @zadjii-msft, I forgot the base event was there too. So, that's good.
Then I think the two scenarios I'm trying to distinguish with would be the difference in requesting more items because we're scrolling and that just being the collection of new items vs. the search changing and the extension wanting to obliterate the existing list and just return new results. Right now, they kind of look the same, right? So, just curious if we want an explicit remove/clear vs. update/add? (Probably ties into whatever thoughts you had on 3 as well.)
Okay so point 3:
What I'm imagining was heavily inspired by the way Terminal's Command Palette filters items. There, when the filter changes, we take the list of items for the updated filter, and "merge" it with the items we're already displaying. We found doing it that way makes the ListView update insanely smoother. Admittedly, Terminal only has strings to check for equality, while we'd have big-ol objects.
What I'm imagining here, is that when CmdPal fetches items in GetItems
, the host will cache the important information into a local object / record / something. (in the current sample, WindowsCommandPalette.ListItemViewModel
reads the properties from the IListItem
into itself, so that if we wanted to read the list item's properties, it's not an IPC call).
If we can make sure that those host-local "wrappers" for an IListItem
can be checked for equality across calls to GetItems
, then I think the extension doesn't need to "clear" the list. Like, if an extension returns just [ObjectA]
in the first call, then [ObjectA, ObjectB]
, we shouldn't need to build a new ListItem wrapper for ObjectA
, and insert that new one. The list already has A in it.
Similarly, if the extension then returns [ObjectC, ObjectD, ObjectB]
, we as the host should be smart enough to discard A from the list, insert C and D at the start, and leave B alone.
Ultimately, I think on the host side, we can keep things seamless to the extension authors. If they just append to the end of the items returned, then we'll just add more items to our listview. If they blow away all the old objects and return new ones, we'll clear the list and display the new ones. If they just insert items into the middle of the results - so will we.
I'm assuming that even though the IListItem
objects are out-of-proc, we can check for equality on the host side trivially. If we can't then we'd need to think about some sort of Title+Subtitle+icon+tags hash.
I think this makes sense in my head, but I can tell that this probably isn't the best explanation. I also have very little depth on the best way to implement stuff in C#. My C++ days might be further complicating my thoughts here.
Sounds good @zadjii-msft, I think we're on the same page here 🙂 with how things should wrap within the host. There'll definitely be some ListItemViewModel that contains the IListItem reference (that was also the benefit of flattening the section, as then we can more easily have the 1:1 mapping between the extension, host, and the UI layer without more indirection.
I think the crux will be when we get the results from GetItems
and its [ObjectA, ObjectB]
and we already have ObjectA
wrapped in the host, how easy is it for us to detect/compare that ObjectA
that we have wrapped is the same ObjectA
returned by the GetItems
call that represents that same item. It probably is just a simple override to the GetHashCode method such that if an item has the same Title+Subtitle, etc... then it's the same.
We could even exploit this such that the ListItemViewModel and ListItem itself compare equally such that we wouldn't have to do unboxing as much between them when trying to find if it exists in the host or not yet. And then only wrap the new ones at the end if we find out they're indeed new.
Description of the new feature / enhancement
We need to continue to improve the current state of the world of our WinRT usage. A lot of questions have come up regarding async await patterns + the idea of being able to 'yield' current in PT Run.
This work item includes investigating the Shmueli.WinRT implementation to see what kind of benefits we may be able to get for passing around types.
Scenario when this would be used?
For all things WinRT with extensions.
Supporting information
"Just copying the hacker news http logic, but would be nice to allow extensions to use proper async code... Maybe ties into if we do filtering and are polling extensions less often in the new model, maybe we have a callback pattern for results/updates, so the extension can just spin off a thread to do a search or longer operation, if needed vs. having to just return data right away?"
The discussion revolved around the possibility of extensions returning items with a group string, allowing the UI to handle the grouping instead of extensions having to structure their data. This could be achieved with a public async Task<IEnumberable> Getitems() method, or possibly an IAsyncOperation for WinRT. The extension author could then use yield return to transpose results from another data model to the item.