-
Notifications
You must be signed in to change notification settings - Fork 236
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
[Proposal][WinUI] Easily create dependency properties #472
base: master
Are you sure you want to change the base?
Conversation
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.
Left my initial questions
// wil::details::Xaml_DependencyObject_SetValue m_setValue{nullptr}; | ||
//}; | ||
|
||
#define WIL_DEFINE_DP(baseClass, type, name) \ |
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.
We have to pass baseClass
to WIL_DEFINE_DP
because we need the winrt::xaml_typename<>
of the base class. This seems like we could derive automatically from any real winrt::implements
class---is there not some sort of this->BaseType
property via C++/WinRT?
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.
we need the xaml_typename but we don't need the class itself.
xaml_typename takes the type as a template param and no arguments
but you could define something like:
template<typename T>
auto xaml_typename_from_object(T&&) {
return winrt::xaml_typename<T>();
}
and just pass xaml_typename_from_object(*this)
or similar
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.
winrt::implements has a class_type
typedef which seems to be what you want here.
My own macro does the following:
# define DECL_DEPENDENCY_PROPERTY_FIELD(TYPE, NAME, METADATA) \
inline static wux::DependencyProperty DEPENDENCY_PROPERTY_FIELD(NAME) = \
wux::DependencyProperty::Register( \
UTIL_STRINGIFY(NAME), \
winrt::xaml_typename<TYPE>(), \
winrt::xaml_typename<class_type>(), \
METADATA);
#define WIL_DEFINE_DP(baseClass, type, name) \ | ||
static wil::details::Xaml_DependencyProperty name##Property() \ | ||
{ \ | ||
static wil::details::Xaml_DependencyProperty s_##name##Property = \ |
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.
I chose to use function-local statics for a few reasons:
- XAML doesn't seem to really care when these statics are initialized: in production, I've seen folks set the statics in the element's constructor, in an "Ensure" function that runs during app load, and even just in the root of the .cpp itself (which I know is dangerous).
- I'm sure this differs from global statics in some respects (is this specific to the translation unit?)---but I'm not sure if that matters for this type of call. What is different because of this change?
- Using function-local statics dramatically simplifies this code.
What should we do here?
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.
which I know is dangerous
Do you mind explaining how this is dangerous? It's what I've mostly seen around.
XAML doesn't seem to really care when these statics are initialized
Is this also true when e.g. the first instance of the class being created is through a .xaml
file and that specific file assigns some DPs? I would've expected XAML to have difficulty there if lazily created since it is gonna try to assign properties that don't have registered metadata yet.
What I do here in my own macro is use C++17's inline static
s, which end up emulating global static behavior but allows you to define it in the header (so I'm wondering if I should move away from this because your statement of them being dangerous).
Also, inline statics aren't specific to the TU.
{ \ | ||
SetValue(name##Property(), winrt::box_value(value)); \ | ||
} \ | ||
static void Ensure##name##Property() \ |
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.
I provide the Ensure###Property()
method to match previous guidance for creating custom DPs. But in practice, calling this in the constructor or near first-use seems to work fine. What are the actual repercussions here?
const DefaultValueType& defaultValue, | ||
const Xaml_PropertyChangedCallback& propertyChangedCallback = nullptr) | ||
{ | ||
// TODO: assert T and PropertyType are compatible |
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.
put a typename = std::enable_if_t<is_assignable_v<PropertyType, DefaultValueType>>
to filter it out
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.
Suggestion: require C++20 and use std::convertible_to<PropertyType> DefaultValueType
Nit: C++/WinRT has no formal documented way to 100% check this, see microsoft/cppwinrt#609
// wil::details::Xaml_DependencyObject_SetValue m_setValue{nullptr}; | ||
//}; | ||
|
||
#define WIL_DEFINE_DP(baseClass, type, name) \ |
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.
the naming here is confusing because I think by baseClass you mean the class that owns the DP, not the base class in an inheritance sense? Same with type, it's ambiguous, I like the naming you had above (propertyType vs ownerType)
// wil::details::Xaml_DependencyObject_SetValue m_setValue{nullptr}; | ||
//}; | ||
|
||
#define WIL_DEFINE_DP(baseClass, type, name) \ |
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.
#define WIL_DEFINE_DP(baseClass, type, name) \ | |
#define WIL_DEFINE_XAML_DP(baseClass, type, name) \ |
Better namespacing
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.
naming wise we probably want to be more specific than DP, i.e. spell out DEPENDENCY_PROPERTY
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.
That's a mouthful, and I think XAML_DP makes it pretty unambiguous.
// We have to do a const_cast because ordinarily SetValue is const--- | ||
// it's delegated to WinRT, and C++/WinRT marks most of its functions | ||
// 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.
this seems odd... SetValue's job is to change a property on the object so the method should not be const...
if it must be, maybe changing m_values to be mutable
instead of the const_cast?
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.
This is cause of the macro itself using const for the property setter. This wouldn't be needed if it weren't.
|
||
template <typename PropertyType, typename OwnerType> | ||
inline Xaml_DependencyProperty register_dependency_property( | ||
const std::wstring_view& propertyNameString) |
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.
std::wstring_view
is trivially copyable. I think it is actually more efficient to pass it by value because it is so cheap to copy.
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.
Correct, taking it by value avoids a stack spill on ARM64: https://godbolt.org/z/e5WbGKz1q
{ | ||
return Xaml_DependencyProperty::Register( | ||
propertyNameString, | ||
winrt::template xaml_typename<PropertyType>(), |
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.
Where is the definition of xaml_typename
coming from?
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.
{ \ | ||
return winrt::unbox_value<type>(GetValue(name##Property())); \ | ||
} \ | ||
void name(type value) 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.
It seems a bit odd that the setter is marked const
. I guess it technically is because the pointer to the DP is not changing.
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.
I recommend not marking it const
yeah - it would be extremely odd and break typical assumptions to have a const foo
but being able to set properties, even if technically it's possible.
} \ | ||
auto name() const \ | ||
{ \ | ||
return winrt::unbox_value<type>(GetValue(name##Property())); \ |
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.
One thing I dislike about overly long macros is that you can't debug them with source code. It can be useful to set debug breakpoints in a getter or setter to see who is changing the value. It won't be possible to (source) debug these DPs.
{ \ | ||
SetValue(name##Property(), winrt::box_value(value)); \ | ||
} \ | ||
static void Ensure##name##Property() \ |
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 there ever a case where an "unensure" function would be needed? For example what happens if XAML is torn down before the module unloads? Destructing the function level static will not be happy.
name##Property(); \ | ||
} | ||
|
||
#define WIL_DEFINE_DP_WITH_DEFAULT_VALUE_AND_CALLBACK(baseClass, type, name, defaultValue, propertyChangedCallback) \ |
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 it valid to provide a default value but not care about the callback (passing null)? Or vice-versa.
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.
From XAML's point of view, yes.
#include <winrt/Windows.UI.Xaml.Data.h> | ||
#include <winrt/Windows.UI.Xaml.Input.h> | ||
#include <winrt/Windows.UI.Xaml.Interop.h> |
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.
I don't think WIL has a WinAppSDK dependency to test against. This coverage is limited to System XAML types.
I think there are already some WinAppSDK helpers in WIL that are similarly impossible to cover with tests. That's unfortunate.
{ | ||
// Throws if not ensured (?) | ||
auto obj = my_dependency_properties{}; | ||
// REQUIRE_FAILFAST_MSG(E_NOTIMPL, [&obj] { obj.MyProperty(); }); |
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.
Are these intended to get uncommented later?
my_dependency_properties::EnsureMyStringPropertyProperty(); | ||
|
||
// Now it should work | ||
obj.MyProperty(42); |
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 is the behavior of querying a property that has not been set yet? Undefined behavior? Should this macro require that the properties be default-constructible or they have to provide a default value if not?
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.
when you register the property you give it its default value
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.
The macro without a default value passes nullptr
for the Windows::UI::Xaml::PropertyMetadata
. That leaves it ambiguous as to the default value. We'd have to check the XAML documentation (assuming the behavior here is publicly documented).
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.
Passing null for this parameter is equivalent to passing a new PropertyMetadata instance created by calling PropertyMetadata.Create with null as the default value parameter.
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.
So calling get without specifying a default value or setting it first would result in winrt::unbox_value<T>(nullptr)
getting called. And that seems to throw E_NOINTERFACE. https://github.com/microsoft/cppwinrt/blob/2e4bdb05da1fe86080c523a2e00f81d62f209b17/strings/base_reference_produce.h#L430
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.
Passing null for this parameter is equivalent to passing a new PropertyMetadata instance created by calling PropertyMetadata.Create with null as the default value parameter.
This means that for ref types, you get a null default value. For value types, unbox_value will throw hresult_no_interface
until a value is set (because you'll try to unbox nullptr).
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.
Oops, those comments hadn't showed up on my end when I was writing this. @dmachaj is correct, with the addition that it wouldn't throw with ref types, but just return nullptr.
} | ||
|
||
//template <typename T> | ||
//struct single_threaded_dependency_property |
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 is the purpose of this big comment? It seems like a full implementation but it is entirely commented out and unused.
#endif | ||
|
||
using Xaml_DependencyObject_GetValue = std::function<winrt::Windows::Foundation::IInspectable(Xaml_DependencyProperty const&)>; | ||
using Xaml_DependencyObject_SetValue = std::function<void(Xaml_DependencyProperty const&, winrt::Windows::Foundation::IInspectable 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.
These seem unused. Also why the type erasure?
propertyNameString, | ||
winrt::template xaml_typename<PropertyType>(), | ||
winrt::template xaml_typename<OwnerType>(), | ||
Xaml_PropertyMetadata{winrt::box_value(defaultValue), propertyChangedCallback}); |
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 a static_cast<PropertyType>
for the default value here, in case nullptr
is passed.
name##Property(); \ | ||
} | ||
|
||
#define WIL_DEFINE_DP_WITH_DEFAULT_VALUE_AND_CALLBACK(baseClass, type, name, defaultValue, propertyChangedCallback) \ |
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.
I suggest folding this and WIL_DEFINE_DP together, lots of repetition otherwise. For example, have them both summon the same macro which uses variadic arguments for the parameters of register_dependency_property.
static wil::details::Xaml_DependencyProperty name##Property() \ | ||
{ \ | ||
static wil::details::Xaml_DependencyProperty s_##name##Property = \ | ||
wil::details::register_dependency_property<type, baseClass>(L"" #name); \ |
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.
Suggest using a stringify helper macro here, in case the dependency property name itself is a macro (it wouldn't be expanded here, leading to a wrong name)
Suggestion: detect the IDL compiler in the header using |
Today, creating a custom dependency property requires a lot of boilerplate. This change gets rid of most of it. With this change, you can add a new dependency property quickly:
Versus before you'd have to (see example on MSDN):
What changed?
WIL_DEFINE_DP
andWIL_DEFINE_DP_WITH_DEFAULT_VALUE_AND_CALLBACK
that automatically create idiomatic implementations of the 3 static & member methods needed for a dependency property.How tested?