- 
                Notifications
    You must be signed in to change notification settings 
- Fork 395
Fix FileTypeChoices in Storage Pickers to preserve insertion order #5948
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
base: main
Are you sure you want to change the base?
Conversation
| size_t FileTypeChoicesMap::FindKeyIndex(hstring const& key) const | ||
| { | ||
| auto it = m_keyToIndex.find(std::wstring(key.c_str())); | ||
| return (it != m_keyToIndex.end()) ? it->second : SIZE_MAX; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor clarification suggestion:
constexpr size_t element_not_found{ SIZE_MAX };
| winrt::Windows::Foundation::Collections::IMapView<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>> FileTypeChoicesMap::GetView() const | ||
| { | ||
| return m_innerMap.GetView(); | ||
| return make<OrderedMapView>(m_orderedMap); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If caller holds GetView return value past FileTypeChoicesMap destruction, you'll have a dangling pointer crash. Consider std::make_shared on m_orderedMap, winrt::make_weak on FileTypeChoicesMap, etc.
| void FileTypeChoicesMap::Remove(hstring const& key) | ||
| { | ||
| m_innerMap.Remove(key); | ||
| size_t index = FindKeyIndex(key); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-atomic reading and/or writing of m_orderedMap & m_keyToIndex - consider a mutex. Same for nearly all other methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or have just one std::vector with a pair of the key and its value. Lookup may theoretically be slower, but the expected count is so low that it is unlikely to matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or have just one std::vector with a pair of the key and its value. Lookup may theoretically be slower, but the expected count is so low that it is unlikely to matter.
that would resolve some scenarios but not all (TOCTOU)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this API an inproc or OOP WinRT object?
If inproc then why should it internally protect instances against concurrent access? The method's not static / no global variables, instances are not typically expected to be accessed concurrently, and typically if a developer does concurrently access an instance of an object from 2 threads managing that concurrent access is their responsbility.
If OOP then yes, every method exposed by every interface on the runtimeclass is potentially access concurrently and needs to protect itself.
|  | ||
| winrt::Windows::Foundation::Collections::IIterator<winrt::Windows::Foundation::Collections::IKeyValuePair<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>> OrderedMapView::First() const | ||
| { | ||
| return make<OrderedMapIterator>(m_map); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another dangling pointer concern
| } | ||
|  | ||
| void OrderedMapView::Split(winrt::Windows::Foundation::Collections::IMapView<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>& first, | ||
| winrt::Windows::Foundation::Collections::IMapView<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>& second) const | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noexcept
| L"Documents", winrt::single_threaded_vector<winrt::hstring>({ L".txt", L".doc", L".docx" })); | ||
| picker.FileTypeChoices().Insert( | ||
| L"Pictures", winrt::single_threaded_vector<winrt::hstring>({ L".png", L".jpg", L".jpeg", L".bmp" })); | ||
| picker.FileTypeChoices().Insert( | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add test case to exercise reuse of existing index
| throw winrt::hresult_out_of_bounds(L"Key not found"); | ||
| } | ||
|  | ||
| uint32_t OrderedMapView::Size() const | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noexcept
| m_keyToIndex.reserve(8); | ||
| } | ||
|  | ||
| size_t FileTypeChoicesMap::FindKeyIndex(hstring const& key) const | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lots of these methods can be made noexcept
|  | ||
| // Helper methods | ||
| auto FindKey(hstring const& key) const; | ||
| auto FindKey(hstring const& key); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not only have the const version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
Usually only need const + non-const for mutating methods. Read-only behavior only needs const method
| void FileTypeChoicesMap::Remove(hstring const& key) | ||
| { | ||
| m_innerMap.Remove(key); | ||
| size_t index = FindKeyIndex(key); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or have just one std::vector with a pair of the key and its value. Lookup may theoretically be slower, but the expected count is so low that it is unlikely to matter.
| return (it != m_keyToIndex.end()) ? it->second : SIZE_MAX; | ||
| } | ||
|  | ||
| auto FileTypeChoicesMap::FindKey(hstring const& key) const | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto return type for a method is cute but harder to reader/grok. That's a long-term maintainability tax which is the wrong vector (pardon the pun) -- generally best for code to be easily maintainable, not hard to maintain.
SUGGESTION: Specify actual return type
same comment applies throughout
same comment
| { | ||
| throw winrt::hresult_out_of_bounds(); | ||
| } | ||
| // Create a simple key-value pair | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Add blank line 139.5
| throw winrt::hresult_out_of_bounds(); | ||
| } | ||
| // Create a simple key-value pair | ||
| auto tempMap = single_threaded_map<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|  | ||
| uint32_t OrderedMapIterator::GetMany(array_view<winrt::Windows::Foundation::Collections::IKeyValuePair<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>> items) | ||
| { | ||
| uint32_t copied = 0; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: uint32_t copied{};
| } | ||
|  | ||
| // OrderedMapView implementation | ||
| auto OrderedMapView::FindKey(hstring const& key) const -> vector_type::const_iterator | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why auto OrderedMapView::FindKey(hstring const& key) const -> vector_type::const_iterator
and not vector_type::const_iterator OrderedMapView::FindKey(hstring const& key) const
?
|  | ||
| // Helper methods | ||
| auto FindKey(hstring const& key) const; | ||
| auto FindKey(hstring const& key); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
Usually only need const + non-const for mutating methods. Read-only behavior only needs const method
|  | ||
| private: | ||
| vector_type const& m_map; | ||
| size_t m_current; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
size_t m_current{};
Always initialize variables
| { | ||
| using vector_type = std::vector<std::pair<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>>; | ||
|  | ||
| OrderedMapView(vector_type const& map) : m_map(map) {} | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why's this method inline but not any others?
e.g. Size() is equally trivial or more so but not inline
SUGGEST: Change this to non-inline for consistency with the rest of the code
| winrt::Windows::Foundation::Collections::IIterator<winrt::Windows::Foundation::Collections::IKeyValuePair<hstring, winrt::Windows::Foundation::Collections::IVector<hstring>>> First() const; | ||
|  | ||
| private: | ||
| vector_type const& m_map; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when the underlying map object passed in to the constructor is destroyed and then this object is used?
I think Scott mentioned this earlier (dangling pointer etc)
Description
This is a fix for issue:
The existing FileTypeChoices property was built on an unordered map, which does not preserve the user-defined order.
However, in the FileOpenPicker and FileSavePicker, the FileTypeChoices need to be displayed in the order they were inserted, rather than in a random order.
This is important for developers' and end-users' experience because:
Additionaly, the insertion order is respected in the legacy UWP FileSavePicker.
Fix
This is a backward-compatible fix. The goal of this fix is to maintain the existing Map type API contract and its good performance while ensuring that the display order of FileTypeChoices meets expectations.
This pull request refactors the implementation of the
FileTypeChoicesMapto ensure that the insertion order of keys is preserved and provides efficient key lookups. It replaces the previous unordered map-based implementation with a custom ordered map backed by a vector and a hash map for O(1) lookups. The changes also introduce custom iterator and view types to maintain order during iteration and map viewing. Corresponding tests are updated to verify the new behavior.Code change details:
Core implementation changes:
FileTypeChoicesMapto use a vector (m_orderedMap) for maintaining insertion order and an unordered map (m_keyToIndex) for fast key lookups, replacing the previoussingle_threaded_map-based approach. Added helper methods for key lookup and index finding. [1] [2]OrderedMapIteratorandOrderedMapViewtypes to support ordered iteration and map viewing, ensuring the public API returns items in insertion order. [1] [2]API and behavior updates:
IMapandIMapViewinterface methods (Insert,Lookup,Size,HasKey,GetView,Remove,Clear,First) to operate on the new ordered data structures and maintain correct semantics.Testing and validation:
These changes ensure that the
FileTypeChoicesMapbehaves as an ordered map, which is important for scenarios where the order of file type choices matters in the UI or API responses.