Add Views Ownership Best Practices

Author: [email protected]
Examples: [email protected]
Markdown: [email protected]
Change-Id: Id6d0b17f37d4bb8f0e11d67b19487af923592d30
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2158060
Commit-Queue: Robert Liao <[email protected]>
Auto-Submit: Robert Liao <[email protected]>
Reviewed-by: Peter Kasting <[email protected]>
Cr-Commit-Position: refs/heads/master@{#761160}
diff --git a/docs/ui/index.md b/docs/ui/index.md
index 2043bf5..12ea5d54 100644
--- a/docs/ui/index.md
+++ b/docs/ui/index.md
@@ -15,7 +15,7 @@
 
 Details on Chrome UI.
 
-* [Coming Soon]
+* [Best Practices](/docs/ui/learn/index.md#best-practices)
 
 Processes
 
diff --git a/docs/ui/learn/bestpractices/ownership.md b/docs/ui/learn/bestpractices/ownership.md
new file mode 100644
index 0000000..6c0a340
--- /dev/null
+++ b/docs/ui/learn/bestpractices/ownership.md
@@ -0,0 +1,624 @@
+# Best Practice: Ownership
+
+In the common case, a `View` instance lives within a hierarchy of `View`s, up to
+a `RootView` object that is owned by a `Widget`. Calling `AddChildView<>()`
+typically passes ownership of the child to the parent, while
+`RemoveChildView[T<>]()` gives ownership back to the caller. While other
+ownership patterns exist, newly-added code should use this one.
+
+[TOC]
+
+## Lifetime Basics
+
+Accordingly, the best practices for ownership and lifetime management in Views
+code are similar to those in the rest of Chromium, but include some details
+specific to the Views APIs.
+
+* **[Avoid bare new.](https://chromium.googlesource.com/chromium/src/+/master/styleguide/c++/c++-dos-and-donts.md#use-and-instead-of-bare)**
+  Use `std::make_unique<>()` when creating objects, and distinguish owning and
+  non-owning uses by [using smart and raw pointer types](https://chromium.googlesource.com/chromium/src/+/master/styleguide/c++/c++.md#object-ownership-and-calling-conventions).
+* **Use [the unique\_ptr<> version of View::AddChildView<>()](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=404;drc=7cba41605a8489bace83f380760486638a2a8a4a).**
+  This clearly conveys ownership transfer from the caller to the parent `View`.
+  It also `DCHECK()`s that `set_owned_by_client()` has not been called,
+  preventing double-ownership.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Typical code from [`dice_bubble_sync_promo_view.cc`](https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/sync/dice_bubble_sync_promo_view.cc;l=49;drc=459f2d16a6592b1290a1c2197231d46f48f49af4)
+using bare `new`:
+
+#####
+
+**Best Practice**
+
+Rewriting using the `unique_ptr<>` version of `AddChildView<>()` is shorter and
+safer:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+...
+views::Label* title =
+    new views::Label(
+        title_text, CONTEXT_BODY_TEXT_LARGE,
+        text_style);
+
+title->SetHorizontalAlignment(
+    gfx::HorizontalAlignment::ALIGN_LEFT);
+title->SetMultiLine(true);
+AddChildView(title);
+...
+signin_button_view_ =
+    new DiceSigninButtonView(
+        account, account_icon, this,
+        /*use_account_name_as_title=*/true);
+AddChildView(signin_button_view_);
+...
+```
+
+#####
+
+``` cpp
+...
+auto* title =
+    AddChildView(
+        std::make_unique<views::Label>(
+            title_text, CONTEXT_BODY_TEXT_LARGE,
+            text_style));
+title->SetHorizontalAlignment(
+    gfx::HorizontalAlignment::ALIGN_LEFT);
+title->SetMultiLine(true);
+
+...
+signin_button_view_ =
+    AddChildView(
+        std::make_unique<DiceSigninButtonView>(
+            account, account_icon, this,
+            /*use_account_name_as_title=*/true));
+...
+```
+
+|||---|||
+
+## Avoid View::set_owned_by_client()
+
+The [`View::set_owned_by_client()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=390;drc=7cba41605a8489bace83f380760486638a2a8a4a)
+flag means that a `View` is owned by something other than its parent `View`.
+This **method is deprecated** (and will eventually be removed) since it results
+in APIs that are easy to misuse, code where ownership is unclear, and a higher
+likelihood of bugs. Needing this flag may be a signal that the `View` subclass
+in question is too heavyweight, and refactoring using MVC or a similar paradigm
+would allow a long-lived "model" or "controller" with more-transient "view"s.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Code in [`time_view.{h,cc}`](https://source.chromium.org/chromium/chromium/src/+/master:ash/system/time/time_view.h;l=35;drc=7d8bc7f807a433e6a127806e991fe780aa27ce77)
+that uses `set_owned_by_client()` to have non-parented `View`s, so it can swap
+layouts without recreating the children:
+
+#####
+
+**Best Practice**
+
+Rewriting using subclasses to encapsulate layout allows the parent to merely
+adjust visibility:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+class ASH_EXPORT TimeView : public ActionableView,
+                            public ClockObserver {
+  ...
+ private:
+
+
+
+  std::unique_ptr<views::Label> horizontal_label_;
+  ...
+  std::unique_ptr<views::Label> vertical_label_hours_;
+  std::unique_ptr<views::Label> vertical_label_minutes_;
+  ...
+};
+
+void TimeView::SetupLabels() {
+  horizontal_label_.reset(new views::Label());
+  SetupLabel(horizontal_label_.get());
+  vertical_label_hours_.reset(new views::Label());
+  SetupLabel(vertical_label_hours_.get());
+  vertical_label_minutes_.reset(new views::Label());
+  SetupLabel(vertical_label_minutes_.get());
+  ...
+}
+
+void TimeView::SetupLabel(views::Label* label) {
+  label->set_owned_by_client();
+  ...
+}
+
+
+
+
+
+
+
+
+
+
+
+void TimeView::UpdateClockLayout(
+    ClockLayout clock_layout) {
+  ...
+  if (clock_layout == ClockLayout::HORIZONTAL_CLOCK) {
+    RemoveChildView(vertical_label_hours_.get());
+    RemoveChildView(vertical_label_minutes_.get());
+    SetLayoutManager(
+        std::make_unique<views::FillLayout>());
+    AddChildView(horizontal_label_.get());
+  } else {
+    RemoveChildView(horizontal_label_.get());
+    // Remove the current layout manager since it could
+    // be the FillLayout which only allows one child.
+    SetLayoutManager(nullptr);
+    // Pre-add the children since ownership is being
+    // retained by this.
+    AddChildView(vertical_label_hours_.get());
+    AddChildView(vertical_label_minutes_.get());
+    views::GridLayout* layout =
+        SetLayoutManager(
+            std::make_unique<views::GridLayout>());
+    ...
+  }
+  Layout();
+}
+```
+
+#####
+
+``` cpp
+class ASH_EXPORT TimeView : public ActionableView,
+                            public ClockObserver {
+  ...
+ private:
+  class HorizontalLabelView;
+  class VerticalLabelView;
+  ...
+  HorizontalLabelView* horizontal_label_;
+  VerticalLabelView* vertical_label_;
+  ...
+};
+
+
+
+TimeView::HorizontalLabelView::HorizontalLabelView() {
+  SetLayoutManager(
+      std::make_unique<views::FillLayout>());
+  ...
+}
+
+TimeView::VerticalLabelView::VerticalLabelView() {
+  views::GridLayout* layout =
+        SetLayoutManager(
+            std::make_unique<views::GridLayout>());
+  ...
+}
+
+void TimeView::TimeView(ClockLayout clock_layout,
+                        ClockModel* model) {
+  ...
+  horizontal_label_ =
+     AddChildView(
+        std::make_unique<HorizontalLabelView>());
+  vertical_label_ =
+     AddChildView(
+        std::make_unique<VerticalLabelView());
+  ...
+}
+
+void TimeView::UpdateClockLayout(
+    ClockLayout clock_layout) {
+  ...
+  const bool is_horizontal =
+      clock_layout == ClockLayout::HORIZONTAL_CLOCK;
+  horizontal_label_->SetVisible(is_horizontal);
+  vertical_label_->SetVisible(!is_horizontal);
+  Layout();
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+|||---|||
+
+## Avoid Refcounting and WeakPtrs
+
+Refcounting and `WeakPtr`s may also be indicators that a `View` is doing more
+than merely displaying UI. Views objects should only handle UI. Refcounting and
+`WeakPtr` needs should generally be handled by helper objects.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Old code in `cast_dialog_no_sinks_view.{h,cc}` that used weak pointers to
+`PostDelayedTask()` to itself:
+
+#####
+
+**Best Practice**
+
+[Current version](https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/media_router/cast_dialog_no_sinks_view.h;l=20;drc=2a147d67e51e67144257d3a405e3aafec3827513)
+eliminates lifetime concerns by using a `OneShotTimer`, which is canceled when
+destroyed:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+class CastDialogNoSinksView
+    : public views::View, public views::ButtonListener {
+  ...
+ private:
+  base::WeakPtrFactory<CastDialogNoSinksView>
+      weak_factory_{this};
+  ...
+};
+
+CastDialogNoSinksView::CastDialogNoSinksView(
+    Profile* profile) : profile_(profile) {
+  ...
+  base::PostDelayedTask(
+      FROM_HERE, {content::BrowserThread::UI},
+      base::BindOnce(
+          &CastDialogNoSinksView::ShowHelpIconView,
+          weak_factory_.GetWeakPtr()),
+      kSearchWaitTime);
+}
+```
+
+#####
+
+``` cpp
+class CastDialogNoSinksView
+    : public views::View, public views::ButtonListener {
+  ...
+ private:
+  base::OneShotTimer timer_;
+  ...
+};
+
+
+CastDialogNoSinksView::CastDialogNoSinksView(
+    Profile* profile) : profile_(profile) {
+  ...
+  timer_.Start(
+      FROM_HERE, kSearchWaitTime,
+      base::BindOnce(
+          &CastDialogNoSinksView::SetHelpIconView,
+          base::Unretained(this)));
+}
+
+```
+
+|||---|||
+
+## Use View::RemoveChildViewT<>()
+
+[`View::RemoveChildViewT<>()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=443;drc=7cba41605a8489bace83f380760486638a2a8a4a)
+**clearly conveys ownership transfer** from the parent `View` to the caller.
+Callers who wish to delete a `View` can simply ignore the return argument. This
+is preferable to calling `RemoveChildView()` and deleting the raw pointer
+(cumbersome and error-prone), calling `RemoveChildView()` without ever deleting
+the pointer (leaks the `View`), or simply deleting a pointer to a still-parented
+`View` (will work today, but is semantically incorrect and may be removed in the
+future).
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Typical code in [`network_list_view.cc`](https://source.chromium.org/chromium/chromium/src/+/master:ash/system/network/network_list_view.cc;l=296;drc=4867a6ce87c51024259259baad08b0ba8ae030a5)
+which manually deletes a child after removing it:
+
+#####
+
+**Best Practice**
+
+Rewriting using `RemoveChildViewT<>()` is shorter and safer:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+  ... if (mobile_header_view_) {
+    scroll_content()->RemoveChildView(
+        mobile_header_view_);
+    delete mobile_header_view_;
+    mobile_header_view_ = nullptr;
+    needs_relayout_ = true;
+  }
+  ...
+```
+
+#####
+
+``` cpp
+  ... if (mobile_header_view_) {
+    scroll_content()->RemoveChildViewT(
+        mobile_header_view_);
+
+    mobile_header_view_ = nullptr;
+    needs_relayout_ = true;
+  }
+  ...
+```
+
+|||---|||
+
+## Prefer Scoping Objects
+
+**Prefer scoping objects to paired Add/Remove-type calls**. For example, use
+a [`ScopedObserver<>`](https://source.chromium.org/chromium/chromium/src/+/master:base/scoped_observer.h;l=43;drc=6bd38d45dbd4144235318b607216e51a7f9dc60e)
+instead of directly calling [`View::AddObserver()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1346;drc=7cba41605a8489bace83f380760486638a2a8a4a)
+and [`RemoveObserver()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1347;drc=7cba41605a8489bace83f380760486638a2a8a4a).
+Such objects reduce the likelihood of use-after-free.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Typical code in [`avatar_toolbar_button.cc`](https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/profiles/avatar_toolbar_button.cc;l=50;drc=f81f0d5d17c037c5866b8808322931f313b796e1)
+that uses paired add/remove calls:
+
+#####
+
+**Best Practice**
+
+Rewriting using `ScopedObserver<>` eliminates the destructor body entirely:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+
+
+
+
+
+
+
+
+
+
+AvatarToolbarButton::AvatarToolbarButton(
+    Browser* browser, ToolbarIconContainerView* parent)
+    : browser_(browser), parent_(parent) {
+  ...
+  if (parent_)
+    parent_->AddObserver(this);
+}
+
+AvatarToolbarButton::~AvatarToolbarButton() {
+  if (parent_)
+    parent_->RemoveObserver(this);
+}
+```
+
+#####
+
+``` cpp
+class AvatarToolbarButton
+    : public ToolbarButton,
+      public ToolbarIconContainerView::Observer {
+  ...
+ private:
+  ScopedObserver<AvatarToolbarButton,
+                 ToolbarIconContainerView::Observer>
+      observer_{this};
+};
+
+AvatarToolbarButton::AvatarToolbarButton(
+    Browser* browser, ToolbarIconContainerView* parent)
+    : browser_(browser), parent_(parent) {
+  ...
+  if (parent_)
+    observer_.Add(parent_);
+}
+
+AvatarToolbarButton::~AvatarToolbarButton() = default;
+
+
+
+```
+
+|||---|||
+
+## Keep Lifetimes Fully Nested
+
+For objects you own, **destroy in the reverse order you created,** so lifetimes
+are nested rather than partially-overlapping. This can also reduce the
+likelihood of use-after-free, usually by enabling code to make simplifying
+assumptions (e.g. that an observed object always outlives `this`).
+
+|||---|||
+
+####
+
+**Avoid**
+
+Old code in `widget_interactive_uitest.cc` that destroys in the same order as
+creation:
+
+#####
+
+**Best Practice**
+
+[Current version](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/widget/widget_interactive_uitest.cc;l=492;drc=cbe5dca6cb1e1511c82776370a964d99cbf5205d)
+uses scoping objects that are simpler and safer:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+TEST_F(WidgetTestInteractive,
+       ViewFocusOnWidgetActivationChanges) {
+  Widget* widget1 = CreateTopLevelPlatformWidget();
+  ...
+
+  Widget* widget2 = CreateTopLevelPlatformWidget();
+  ...
+  widget1->CloseNow();
+  widget2->CloseNow();
+}
+```
+
+#####
+
+``` cpp
+TEST_F(WidgetTestInteractive,
+       ViewFocusOnWidgetActivationChanges) {
+  WidgetAutoclosePtr widget1(
+      CreateTopLevelPlatformWidget());
+  ...
+  WidgetAutoclosePtr widget2(
+      CreateTopLevelPlatformWidget());
+  ...
+}
+
+```
+
+|||---|||
+
+## Only Use Views Objects on the UI Thread
+
+**Always use `View`s on the main (UI) thread.** Like most Chromium code, the
+Views toolkit is [not thread-safe](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=170;drc=7cba41605a8489bace83f380760486638a2a8a4a).
+
+## Add Child Views in a View's Constructor
+
+In most cases, **add child `View`s in a `View`'s constructor.** From an
+ownership perspective, doing so is safe even though the `View` is not yet in a
+`Widget`; if the `View` is destroyed before ever being placed in a `Widget`, it
+will still destroy its child `View`s. Child `View`s may need to be added at
+other times (e.g. in [`AddedToWidget()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1439;drc=7cba41605a8489bace83f380760486638a2a8a4a)
+or [`OnThemeChanged()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1545;drc=7cba41605a8489bace83f380760486638a2a8a4a),
+if constructing the `View` requires a color; or lazily, if creation is expensive
+or a `View` is not always needed); however, do not copy any existing code that
+adds child `View`s in a [`ViewHierarchyChanged()`](https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1422;drc=7cba41605a8489bace83f380760486638a2a8a4a)
+override, as such code is usually an artifact of misunderstanding the Views
+ownership model.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Typical code in [`native_app_window_views.cc`](https://source.chromium.org/chromium/chromium/src/+/master:extensions/components/native_app_window/native_app_window_views.cc;l=285;drc=d66100ebd950c39b80cfd1c72cec618b33eb3491)
+that sets up a child `View` in `ViewHierarchyChanged()`:
+
+#####
+
+**Best Practice**
+
+Rewriting to do this at construction/`Init()` makes |`web_view_`|'s lifetime
+easier to reason about:
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+void NativeAppWindowViews::ViewHierarchyChanged(
+    const views::ViewHierarchyChangedDetails& details) {
+  if (details.is_add && details.child == this) {
+    DCHECK(!web_view_);
+    web_view_ =
+        AddChildView(
+            std::make_unique<views::WebView>(nullptr));
+
+
+
+
+
+
+
+    web_view_->SetWebContents(
+        app_window_->web_contents());
+  }
+}
+```
+
+#####
+
+``` cpp
+
+
+NativeAppWindowViews::NativeAppWindowViews() {
+  ...
+  web_view_ =
+      AddChildView(
+          std::make_unique<views::WebView>(nullptr));
+}
+
+void NativeAppWindowViews::Init(
+    extensions::AppWindow* app_window,
+    const extensions::AppWindow::CreateParams&
+        create_params) {
+  app_window_ = app_window;
+  web_view_->SetWebContents(
+      app_window_->web_contents());
+  ...
+}
+```
+
+|||---|||
diff --git a/docs/ui/learn/index.md b/docs/ui/learn/index.md
index 0a38a5c..bf05026 100644
--- a/docs/ui/learn/index.md
+++ b/docs/ui/learn/index.md
@@ -1,4 +1,8 @@
 # Learning about Chromium UI
 
+# Best Practices
+
+* [Ownership](bestpractices/ownership.md): How to manage Views object lifetimes.
+
 # Architecture Overview
 Coming soon.