1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
|
## Views Dialog Class Splitting
This document outlines how to refactor a common old Views design pattern into a
cluster of smaller objects with less individual responsibility that are easier
to test. The result is broadly similar to the model-view-controller paradigm but
with some Views-specific differences.
This document is specifically applicable to dialogs, bubbles, and secondary UI
surfaces that have their own Widgets. The techniques described here work well
for subclasses of:
* `WidgetDelegate`
* `DialogDelegate`
* `BubbleDialogDelegate`
This document generally uses `DialogDelegate` throughout.
### The Old Pattern: DialogDelegateView Controllers
Legacy Views dialogs often have a class like this:
```c++
class MyDialogView : public views::DialogDelegateView,
public SomeViewListener,
public MyModelListener {
public:
MyDialogView(MyModel* model);
~MyDialogView() override;
// DialogDelegate:
std::u16string GetWindowTitle() const override;
bool ShouldShowCloseButton() const override;
...
// MyViewListener:
void OnMyViewClicked(MyView* view) override;
// MyModelListener:
void OnMyModelChanged(MyModel* model) override;
private:
MyModel* model_;
Label* status_;
...
};
void MyDialogView::OnMyViewClicked(MyView* view) {
// ... complex business logic ...
model_->Update(new_state);
}
void MyDialogView::OnModelChanged(Model* model) {
// ... complex presentation logic ...
}
```
### The Motivation & The Single Responsibility Principle
The single responsibility principle is as follows: each class should have one
responsibility. The class given above has several responsibilities:
* It functions as a `DialogDelegate` for a given `Widget`
* It functions as a `View` within that `Widget`
* It handles translating user actions on the dialog to model updates
* It handles translating model updates into visual changes in the dialog
To make matters worse, classes of this pattern often do this in their
constructor:
```c++
MyDialogView::MyDialogView(MyModel* model) {
// ... lots of setup ...
views::DialogDelegate::CreateDialogWidget(this, context, parent)->Show();
}
```
The last line of code of the constructor packs a lot of meaning:
* `CreateDialogWidget` does or does not take ownership of `this`, depending on
whether `MyDialogView` overrides `DeleteDelegate`
* By default, `DialogDelegateView` uses itself as the contents view of the
created widget
* The created widget takes ownership of itself, and is shown on screen as a
side-effect of the constructor
Doing this makes classes of this pattern exceptionally difficult to test.
### The New Pattern: Decomposed Classes
Here's how this class might look in "new style", using callbacks rather than
observer/listener interfaces, and using composition instead of inheritance for
both `View` and `DialogDelegate`:
```c++
class MyDialog {
public:
MyDialog(MyModel* model);
~MyDialog();
void Show(gfx::NativeWindow context, gfx::NativeView parent);
private:
void OnModelChanged(MyModel* model);
void OnMyViewClicked(MyView* view);
std::unique_ptr<View> MakeContentsView();
std::unique_ptr<DialogDelegate> MakeDialogDelegate();
base::CallbackListSubscription model_subscription_;
std::unique_ptr<DialogDelegate> delegate_;
Widget* widget_ = nullptr; // if needed
const Model* model_; // usually needed
Label* status_ = nullptr; // or similar Views that are needed later
};
MyDialog::MyDialog(MyModel* model) : model_(model) {
model_subscription_ = model->RegisterUpdateCallback(
base::BindRepeating(&MyDialog::OnModelChanged, base::Unretained(this)));
delegate_ = MakeDialogDelegate(MakeContentsView());
}
void MyDialog::Show(gfx::NativeWindow context, gfx::NativeView parent) {
widget_ = views::DialogDelegate::CreateDialogWidget(
std::move(delegate_), context, parent);
widget_->Show();
// Or if we don't need to store widget_ we could just do:
views::DialogDelegate::CreateDialogWidget(
std::move(delegate_), context, parent)->Show();
}
std::unique_ptr<View> MyDialog::MakeContentsView() {
// Create the contents view, set up its LayoutManager, create any needed
// subviews, and store weak pointers to them. For example:
auto contents = std::make_unique<BoxLayoutView>();
auto status = std::make_unique<Label>();
status->ConfigureAsDesired();
status_ = contents->AddChildView(std::move(status));
// We don't need a reference to this view after creation time so we don't
// bother storing it:
auto help = std::make_unique<MyView>(
base::BindRepeating(&MyDialog::OnMyViewClicked, base::Unretained(this)));
contents->AddChildView(std::move(help));
}
std::unique_ptr<DialogDelegate> MyDialog::MakeDialogDelegate(
std::unique_ptr<View> contents) {
// Create a DialogDelegate and configure it as needed:
auto delegate = std::make_unique<DialogDelegate>();
delegate->SetContentsView(std::move(contents));
delegate->SetShowCloseButton(...);
delegate->SetTitle(...);
return delegate;
}
```
Now `MyDialog` has a single responsibility: it ties together a `DialogDelegate`,
a `View`, and a `MyModel`. The ownership of `MyDialog` is now very clear:
`MyDialog` is always owned by the client regardless of what happens with the
underlying `Widget`. The created `DialogDelegate` is owned by `MyDialog` unless
and until it is handed off to a `Widget`. The contents view (and hence all the
other views) are owned by the `DialogDelegate` until they are handed off (by
`DialogDelegate` itself) to the `Widget`'s `RootView`.
### Going Stateless
Some dialogs don't actually have any meaningful internal state. For example,
let's suppose we are displaying a dialog to the user that prompts them for a
text string, then calls a method on a given controller object with that text
string.
In the "old style" that class might look like:
```c++
class MyPromptView : public DialogDelegateView {
public:
static void Show(MyController* controller,
gfx::NativeWindow context,
gfx::NativeView parent);
private:
MyPromptView(MyController* controller);
~MyPromptView() override;
void OnDialogAccepted();
Textfield* field_;
MyController* controller_;
};
// static
void MyPromptView::Show(MyController* controller, gfx::NativeWindow context,
gfx::NativeView parent) {
views::DialogDelegate::CreateDialogView(
new MyPromptView(controller), context, parent)->Show();
}
MyPromptView::MyPromptView(MyController* controller) : controller_(controller) {
SetDialogButtons(ui::DIALOG_BUTTON_OK);
SetAcceptCallback(base::BindOnce(
&MyPromptView::OnDialogAccepted, base::Unretained(this)));
SetLayoutManager(...);
field_ = AddChildView(std::make_unique<Textfield>(...));
AddChildView(std::make_unique<Label>(...));
}
void MyPromptView::OnDialogAccepted() {
controller_->OnUserEnteredText(field_->GetText());
}
```
Note that actually, the entire public interface of `MyPromptView` is a single
static method, so if we use composition instead, we get:
### The Stateless Way
```c++
namespace {
void OnDialogAccepted(MyController* controller, Textfield* field) {
controller->OnUserEnteredText(field->GetText());
}
std::unique_ptr<DialogDelegate> MakePromptDialog(MyController* controller) {
auto contents = std::make_unique<View>(...);
contents->SetLayoutManager(...);
auto* field = contents->AddChildView(std::make_unique<Textfield>(...));
contents->AddChildView(std::make_unique<Label>(...));
auto delegate = std::make_unique<DialogDelegate>(...);
delegate->SetAcceptCallback(base::BindOnce(&OnDialogAccepted,
base::Unretained(controller), base::Unretained(field)));
return delegate;
}
}
void ShowPromptDialog(MyController* controller, gfx::NativeWindow context,
gfx::NativeView parent) {
DialogDelegate::CreateDialogWidget(MakePromptDialog(controller),
context, parent)->Show();
}
```
... the entire dialog class vanishes in a puff of refactoring, replaced by a
single function!
### Refactoring step-by-step
Let's say we are refactoring `MyDialogDelegateView` and want to produce
`MyDialog`. Here we'll assume `MyDialogDelegateView` subclasses
`DialogDelegateView`, but these steps work with any other `WidgetDelegate`
subclass.
1. Replace every override of a `DialogDelegate` method with a call to one of the
new setter methods from that class. This can require some care, since the
values of getters may depend on state that is not available until after
construction time.
2. Have `MyDialogDelegateView` construct a separate `DialogDelegate` as needed,
possibly storing a reference to it for later use if needed. Migrate all the
setup code from (1) to target that new, separate delegate instead. Have
`MyDialogDelegateView` retain ownership of this new `DialogDelegate` member.
3. Have any code that uses `MyDialogDelegateView` as a `DialogDelegate` instead
use the new `DialogDelegate` member of that class.
4. Make `MyDialogDelegateView` not inherit from `DialogDelegate`, and rename it
to `MyDialogView` (inheriting from View rather than `DialogDelegateView`).
Since `MyDialogView` is still used as the `DialogDelegate`'s contents view,
instances of `MyDialogView` will end up owned by `Views`.
5. Have `MyDialogView` construct a separate `View` to be the dialog's contents
view, rather than using itself. Change all calls to `View` methods on
`MyDialogView` to instead target the new contents view member, and have all
external users that treat `MyDialogView` as a `View` instead use that View
member.
6. Make `MyDialogView` not inherit from `View`, and rename it to `MyDialog`.
Note that instances of `MyDialog` are not owned by anyone now, which is bad.
If the class still needs to exist at all, give it ownership semantics;
otherwise, it may be possible to refactor the `MyDialog` class away entirely
into a single function that creates the `DialogDelegate` and `View` members,
sets up the `Widget`, and returns.
|