Open dotMorten opened 5 years ago
This would be really great to add to UWP. It is sorely missed and results in ugly, half-functional code such as the below. Both IsVisible and IsVisibleChanged would be a boost in a number of scenarios related to control development/optimization.
WPF has three different Visibility states (opposed to the current two in UWP): Visible, Hidden, Collapsed.
I don't know about the plans for the UWP Visibility enum, (and how the talk of bringing feature parity with WPF to UWP would affect it) but this implementation would make the API proposed above more future-proof:
public class UIElement
{
+ public bool IsVisible { get; }
+ public event EventHandler<VisibilityChangedEventArgs> VisibilityChanged;
}
+ public class VisibilityChangedEventArgs : EventArgs
{
+ public Visibility OldState { get; }
+ public Visibility NewState { get; }
}
Thank you for filing this issue. I agree with the rationale that determining if something is visible, like actually really visible is difficult to do on the app-side for the reasons mentioned (like monitoring the ancestor chain). Additionally, we'd also want to account for things like elements contained within open/closed popups when we report if the element is Visible.
Although this feature request is righteous, it would require changes in the underlying platform meaning the earliest we'd do this work is post-WinUI 3.0. (The roadmap can be found here: #717 ) In the meantime, we will keep this item open but in the Freezer for now.
Note that a related and also useful feature would be the ability to see if an element is on screen, and this proposal is not that; this IsVisible API doesn't detect that an element is clipped or occluded. Clipping is more detectable now with FrameworkElement.EffectiveViewportChanged, but occlusion would be expensive to calculate.
WPF has three different Visibility states (opposed to the current two in UWP): Visible, Hidden, Collapsed.
WPF's Visibility has three states to match the HTML behavior of both display:none (Visibility.Collapsed) and visibility:hidden (Visibility.Hidden). Visibility.Hidden has generally been considered a mistake because you can get the same thing by setting the Opacity to 0, and because the enum is more difficult to work with than a bool.
I agree. In my opinion, this is a significant problem and a solution would be great to have. But is it possible that a workaround already exists? Is it possible to use Windows.UI.Composition.Visual.IsVisible as a partial workaround? I don't know the answer because the documentation for Visual.IsVisible
is only a single sentence, and the source code of WinUI is not yet public. The documentation doesn't explain how this property is affected by the visibility of ancestor Visual
instances.
Given an instance of a class derived from FrameworkElement
or UIElement
, how can we get the corresponding Visual
in order to invoke Visual.IsVisible
? And if an ancestor element is collapsed, do descendent elements have NO corresponding Visual
at all? Can an app say that an element is hidden if it has NO corresponding Visual
?
I do realize that Visual
has only IsVisible
and not an event IsVisibleChanged
, but nevertheless Visual.IsVisible
could still be usable as a partial workaround, even without the event. For example, imagine a clock element that displays the current time including seconds (either digital or analog; doesn't matter). Such a clock element must use a repeating timer that is triggered once per second. Obviously, each time the timer is triggered, it updates the clock graphic or text. Thus the clock element could invoke Visual.IsVisible
each time the timer is triggered, once per second. When Visual.IsVisible
returns false, the clock element stops its own timer.
This workaround is useful but less-good than an event IsVisibleChanged
because the lack of the event means that the clock element auto-stops but doesn't auto-start (the clock element must be manually re-started later). Whereas if the event existed, then the clock element would BOTH auto-start and auto-stop. As I said, it's a workaround not a proper full solution. But more importantly, how exactly does Visual.IsVisible
behave?
@dotMorten wrote:
I think the WPF equivalent APIs (linked above) are sufficient in behavior and a (near) identical API should be added to UWP:
The name IsVisible
is easily confused with the Visibility
property, therefore I suggest renaming it to IsActuallyVisible
and IsActuallyVisibleChanged
. This naming is consistent with the ActualWidth
/ActualHeight
properties. Alternatively, it could be named IsReallyVisible
but this name might be too strong, especially if it doesn't detect when an element is clipped or occluded.
@MikeHillberg
Note that a related and also useful feature would be the ability to see if an element is on screen, and this proposal is not that; this IsVisible API doesn't detect that an element is clipped or occluded.
I suggest that it might be best to put the clipping and occlusion testing functionality in a separate feature -- separate to the proposed UIElement.IsVisible
feature. As you mentioned, occlusion would be expensive to calculate. Considering the difficulty and expense of calculating clipping and occlusion, and considering that this check isn't always needed, it may be best to implement it separately.
If the UIElement.IsVisible
feature is made too complicated/difficult, then app developers may suffer a long delay before the feature is released. When it is eventually released, developers might also suffer reliability problems/bugs as a consequence of the complexity. To avoid these problems, clipping and occlusion testing could be excluded from this proposal #674, and instead included in a separate feature request.
@Felix-Dev suggested:
public class VisibilityChangedEventArgs : EventArgs { public Visibility OldState { get; } public Visibility NewState { get; } }
I would find those properties confusing because it is unclear whether NewState
is identical to UIElement.Visibility
versus whether it is intended to report the actual visibility that is affected by ancestors. If NewState
is intended to be identical to UIElement.Visibility
, then it seems redundant/unnecessary because you can just simply invoke UIElement.Visibility
. Furthermore, if this event is triggered via a deferred scheduling mechanism or queue instead of immediately executed, then the info in VisibilityChangedEventArgs
could be old and out-of-date. Therefore the simpler and more reliable solution is to leave the EventArgs empty and instead just let apps invoke UIElement.Visibility
and/or UIElement.IsVisible
-- this makes it clear that the info isn't out-of-date.
I also suggest that it might be better to define the property in the opposite sense. Instead of reporting whether the element is visible, report whether it is hidden/invisible. At first glance, this reversal appears to make no difference, but actually a difference does exist: The accuracy. The reasoning is that the hidden state is easier to accurately detect/determine than the visible state. If an IsInvisible
or IsActuallyHidden
property returns true, then the element is definitely hidden/invisible, but if it returns false, then the element is possibly or probably visible but not guaranteed because the element might be invisible via clipping or occlusion. Thus sometimes true
is given a more accurate definition than false
, and false
is not always the 100% exact opposite of true
. Giving the property the opposite name has useful implications.
@verelpode
One of my ideas with the event args was to give the developer info about the previous visibility state of the UI element without having to manually track it while future-proofing this API. Of course, if the visibility of an UI element can only ever have two possible values (visible & collapsed) then there is no need for the OldState
property. Combined with your good point about the NewState
property, the event args can be left empty as you suggested.
The name IsVisible is easily confused with the Visibility property
I don't really agree with that. Seems pretty obvious to me for a read-only property. I don't see the reason for adding support for potential new future visibility states. None of those would matter in the user-stories described above: It's all about whether it's currently rendering or not.
There's also something to be said of precedence in WPF that has proven what it has works fine, despite WPF actually has more than just two visibility states.
@dotMorten
I meant if the name is IsVisible
, then people could accidentally think that IsVisible
is implemented simply as follows, and I would immediately forgive anyone who makes this mistake, because it seems like an easy mistake to make.
public bool IsVisible
{
get {
return this.Visibility == Visibility.Visible;
}
}
It's all about whether it's currently rendering or not.
OK, then maybe you'd prefer a name like "IsRendered" or "IsCurrentlyRendered" ?
More importantly, what do you think of the possibility of using Windows.UI.Composition.Visual.IsVisible
as a partial workaround until a proper solution is released? I don't know whether it can truly be used for this purpose.
If there is a property, then ActualVisibility
would fit better, and as an enum to allow more complex options in the future.
Thinking about it as a query to the renderer, you could have enum values like:
<element>
Whilst the control itself knows if it is supposed to be Visible, Hidden, or Collapsed - the renderer would keep track of the more detailed visible state, which the control/code can query.
@mdtauk -- I like your choice of naming and the idea of returning an enum, but I can think of a downside. If it reports occlusion, but occlusion testing is expensive to calculate, and apps don't always need this much info, then the ActualVisibility
property would perform expensive calculations even when unnecessary. The cost is even worse when you consider the ongoing calculations necessary to continually calculate occlusion in order to trigger the event ActualVisibilityChanged
whenever the occlusion changes -- not only calculated when the ActualVisibility
property is read, but calculated all the time in order to trigger the event.
To help solve this, the idea might be changed from a property to a method with a parameter to indicate whether a full test should be performed. Possibly something like the following:
public ActualVisibility GetActualVisibility(bool fullCheck);
// Alternatively:
public ActualVisibility CalculateActualVisibility(bool fullCheck);
The above still has a problem: It's unclear whether the event ActualVisibilityChanged
is triggered via a full check or only a quick check. So then must we have another property bool IsFullActualVisibilityCheckPerformed
? This seems complex and messy and it could result in an unreliable implementation.
If a full ActualVisibility
enum is too difficult or time-consuming for the WinUI team to implement, then I'd be happy to have a simple boolean IsInvisible
property.
@verelpode For what its worth, I wasn't suggesting Occluded need to be included, only that as an enum, it could be one of several statuses that could be reported. I imagine it would be a ReadOnly property, but the renderer would be responsible for reporting what the ActualVisibility would be.
The control or codebehind need not do any work to calculate it.
The renderer should know something like an occlusion, as it would be drawing pixels over the control, or only drawing a partial amount of pixels. Occlusion would be different from Clipping, and is probably more of a Holographic state, with XAML elements on top of other elements.
Terminal is interested in this. Right now, we are rendering terminal content even when our controls are completely occluded or missing from the visual tree.
While working on the new shadow implementation for the Toolkit, I noticed that when an element is repositioned in a Canvas (left/top modified) it doesn't seem to trigger a LayoutUpdate
on the child element. That means even those cases get even harder to detect when something has moved.
It's a bit related to this same issue. Basically there needs to be a way to track whenever an element's visibility or position changes relative to the screen/viewport regardless of what may have caused it (user interaction with another control, window resizing/moving, parent control re-layouts or moves a child item, etc...)
Related to #2900
@michael-hawker If a parent only moves (but doesn't change size), why is a layout step needed on the child?
@dotMorten I'm not saying it should call UpdateLayout
but fire a LayoutUpdate
event as the effective layout has been updated:
Occurs when the layout of the visual tree changes, due to layout-relevant properties changing value or some other action that refreshes the layout.
We wouldn't need to re-measure things, but we need a way of understanding that something about the UI has moved. For instance with the composition shadow, that's coordinated via a sibling element, so it needs to be updated. But in this Canvas scenario, there's no way I know of how to detect that it's moved...
I think EffectiveViewportchanged only works in a ScrollViewer
so maybe it needs to be expanded or we could create a different event if it doesn't make sense to modify LayoutUpdate
. However, if you have a StackPanel
and collapse element 1, the LayoutUpdate
fires on element 2 still even though it's size hasn't changed, I feel like it was just overlooked in some of these other cases, for instance with Canvas
.
@stmoy Could this be reconsidered? It would allow a nice performance improvement for us and save battery by being better at stop expensive processes when not connected to the view.
Summary
There's currently no good way to know if a control is currently visible and should be rendering, without having to constantly walk the entire visual tree. This prevents you from reacting to parent controls collapsing a control (think tab and collapse controls), where you for instance might want to stop an expensive SwapChainPanel rendering loop.
Rationale
WPF has always had a read-only
bool UIElement.IsVisible
link and an eventIsVisibleChanged
link that allowed control developers to effectively stop/pause unnecessary work if a control isn't visibly active. Like stop an animation, pause DirectX rendering etc, and not waste battery at that point in time.In UWP there's not really a good way to do this today, except walking the entire UI tree each time the
LayoutUpdated
event fires. You're really left with only being able to react to loaded/unloaded events.You can detect if a specific control gets collapsed, but you'd have to monitor every single parent UI Element as well, so it's not really a practical approach.
Scope
Functional Requirements
I think the WPF equivalent APIs (linked above) are sufficient in behavior and a (near) identical API should be added to UWP: