Skip to content

Commit e626a5e

Browse files
committed
Merge pull request picoe#2259 from cwensley/curtis/mac-grid-contextmenu-without-header
Mac: Fix crash when setting context menu without header
2 parents 79e2c5c + ed9f225 commit e626a5e

File tree

7 files changed

+176
-26
lines changed

7 files changed

+176
-26
lines changed

src/Eto.Mac/Forms/Controls/GridHandler.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ public bool ShowHeader
415415
{
416416
if (value && Control.HeaderView == null)
417417
{
418-
Control.HeaderView = headerView = new EtoTableHeaderView { Handler = this };
418+
Control.HeaderView = headerView = new EtoTableHeaderView { Handler = this, Menu = ContextMenu.ToNS() };
419419
}
420420
else if (!value && Control.HeaderView != null)
421421
{
@@ -437,7 +437,8 @@ public virtual ContextMenu ContextMenu
437437
{
438438
Widget.Properties.Set(GridHandler.ContextMenu_Key, value);
439439
Control.Menu = value.ToNS();
440-
Control.HeaderView.Menu = value.ToNS();
440+
if (Control.HeaderView != null)
441+
Control.HeaderView.Menu = value.ToNS();
441442
}
442443
}
443444

src/Eto/Forms/Binding/BindableBinding.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ public override DualBinding<TValue> Bind(DirectBinding<TValue> sourceBinding, Du
7878
{
7979
GettingNullValue = defaultControlValue,
8080
SettingNullValue = defaultContextValue,
81-
DataItem = contextBinding.DataValue
81+
GetDataItem = () => contextBinding.DataValue // don't actually store the data context object in the binding
8282
};
83+
// don't trigger the value changes when we are currently changing the context
84+
valueBinding.Changing += (sender, e) => e.Cancel = control.IsDataContextChanging;
85+
contextBinding.DataValueChanged += (sender, e) => valueBinding.TriggerDataValueChanged();
86+
8387
DualBinding<TValue> binding = Bind(sourceBinding: valueBinding, mode: mode);
84-
contextBinding.DataValueChanged += delegate
85-
{
86-
((ObjectBinding<object, TValue>)binding.Source).DataItem = contextBinding.DataValue;
87-
};
8888
control.Bindings.Add(contextBinding);
8989
return binding;
9090
}

src/Eto/Forms/Binding/BindableWidget.cs

+25
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ internal protected set
9999
if (Properties.TrySet(Parent_Key, value))
100100
{
101101
if (!HasDataContext && !(DataContext is null))
102+
{
103+
IsDataContextChanging = true;
102104
OnDataContextChanged(EventArgs.Empty);
105+
IsDataContextChanging = false;
106+
}
103107
}
104108
}
105109
}
@@ -193,12 +197,17 @@ public object DataContext
193197
set
194198
{
195199
if (Properties.TrySet(DataContext_Key, value))
200+
{
201+
IsDataContextChanging = true;
196202
OnDataContextChanged(EventArgs.Empty);
203+
IsDataContextChanging = false;
204+
}
197205
}
198206
}
199207

200208
internal bool HasDataContext => Properties.ContainsKey(DataContext_Key);
201209

210+
static readonly object IsDataContextChanging_Key = new object();
202211
static readonly object Bindings_Key = new object();
203212

204213
/// <summary>
@@ -213,6 +222,22 @@ internal void TriggerDataContextChanged()
213222
if (!HasDataContext)
214223
OnDataContextChanged(EventArgs.Empty);
215224
}
225+
226+
/// <summary>
227+
/// Gets a value indicating that the <see cref="DataContext"/> property is changing.
228+
/// </summary>
229+
/// <remarks>
230+
/// This can be used to determine when to allow certain logic during the update of the data context.
231+
///
232+
/// It is used to disable binding setters on the model when the data context changes so that a binding
233+
/// does not cause the view model to be updated when the state hasn't been fully set yet.
234+
/// </remarks>
235+
/// <value><c>true</c> if the DataContext is currently changing, <c>false</c> otherwise.</value>
236+
public bool IsDataContextChanging
237+
{
238+
get => Properties.Get<bool?>(IsDataContextChanging_Key) ?? (Parent as IBindable)?.IsDataContextChanging ?? false;
239+
set => Properties.Set(IsDataContextChanging_Key, value);
240+
}
216241

217242
/// <summary>
218243
/// Unbinds any bindings in the <see cref="Bindings"/> collection and removes the bindings

src/Eto/Forms/Binding/IBindable.cs

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ public interface IBindable
1616
/// For example, a Control may return the data context of a parent, if it is not set explicitly.
1717
/// </remarks>
1818
object DataContext { get; set; }
19+
20+
/// <summary>
21+
/// Gets a value indicating that the <see cref="DataContext"/> property is changing.
22+
/// </summary>
23+
/// <remarks>
24+
/// This can be used to determine when to allow certain logic during the update of the data context.
25+
///
26+
/// It is used to disable binding setters on the model when the data context changes so that a binding
27+
/// does not cause the view model to be updated when the state hasn't been fully set yet.
28+
/// </remarks>
29+
/// <value><c>true</c> if the DataContext is currently changing, <c>false</c> otherwise.</value>
30+
bool IsDataContextChanging { get; }
1931

2032
/// <summary>
2133
/// Event to handle when the <see cref="DataContext"/> has changed

src/Eto/Forms/Binding/ObjectBinding.cs

+46-2
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,66 @@ internal override object InternalValue
103103
public IndirectBinding<TValue> InnerBinding { get; private set; }
104104

105105
/// <summary>
106-
/// Gets the object to get/set the values using the <see cref="InnerBinding"/>
106+
/// Gets or sets the object to get/set the values using the <see cref="InnerBinding"/>
107107
/// </summary>
108+
/// <remarks>
109+
/// This uses <see cref="GetDataItem"/> if set, otherwise it will use the set value.
110+
/// Setting the value explicitly will set <see cref="GetDataItem"/> to null.
111+
/// </remarks>
108112
public T DataItem
109113
{
110-
get { return dataItem; }
114+
get
115+
{
116+
if (GetDataItem != null)
117+
return GetDataItem();
118+
return dataItem;
119+
}
111120
set
112121
{
113122
var hasValueChanged = dataValueChangedHandled;
114123
if (hasValueChanged)
115124
RemoveEvent(DataValueChangedEvent);
116125
dataItem = value;
126+
GetDataItem = null;
117127
OnDataValueChanged(EventArgs.Empty);
118128
if (hasValueChanged)
119129
HandleEvent(DataValueChangedEvent);
120130
}
121131
}
132+
133+
/// <summary>
134+
/// Gets or sets a delegate used to get the current <see cref="DataItem"/> value instead of storing the value in this binding.
135+
/// </summary>
136+
/// <remarks>
137+
/// This is used so that all bindings using a DataContext will be updated with the correct value as soon as the DataContext is set.
138+
/// For example, if updating one binding would trigger the setter of a secondary binding that hasn't been updated with the new value yet
139+
/// it would inadvertently change the DataContext even though it is now actually null.
140+
///
141+
/// Use the <see cref="TriggerDataValueChanged"/> method to specify that the value returned by this delegate has changed,
142+
/// which will cause the binding to update all of its dependants.
143+
///
144+
/// Note if this is set, then it will always be used to retrieve the DataItem. If you set the DataItem directly, this will be set to null
145+
/// and will no longer be used..
146+
/// </remarks>
147+
/// <value>The delegate used to get the current data item</value>
148+
public Func<T> GetDataItem { get; set; }
149+
150+
/// <summary>
151+
/// Triggers the <see cref="DirectBinding{T}.DataValueChanged"/> event.
152+
/// </summary>
153+
/// <remarks>
154+
/// When using the <see cref="GetDataItem"/> delegate to retrieve the value of the DataItem, you can call this method to trigger
155+
/// that its value has been changed.
156+
/// </remarks>
157+
public void TriggerDataValueChanged()
158+
{
159+
var hasValueChanged = dataValueChangedHandled;
160+
if (hasValueChanged)
161+
RemoveEvent(DataValueChangedEvent);
162+
OnDataValueChanged(EventArgs.Empty);
163+
if (hasValueChanged)
164+
HandleEvent(DataValueChangedEvent);
165+
}
122166

123167
/// <summary>
124168
/// Gets or sets the default value to use when setting the value for this binding when input value is null

src/Eto/Forms/Command.cs

+46-16
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Eto.Forms
1010
/// </summary>
1111
public class CheckCommand : Command, IValueCommand<bool>
1212
{
13-
#region Events
13+
#region Events
1414

1515
/// <summary>
1616
/// Occurs when the <see cref="Checked"/> value has changed.
@@ -27,9 +27,9 @@ protected virtual void OnCheckedChanged(EventArgs e)
2727
CheckedChanged(this, e);
2828
}
2929

30-
#endregion
30+
#endregion
3131

32-
#region Properties
32+
#region Properties
3333

3434
bool ischecked;
3535

@@ -50,7 +50,7 @@ public bool Checked
5050
}
5151
}
5252

53-
#endregion
53+
#endregion
5454

5555
/// <summary>
5656
/// Initializes a new instance of the <see cref="Eto.Forms.CheckCommand"/> class.
@@ -157,7 +157,7 @@ public override ToolItem CreateToolItem()
157157
/// </remarks>
158158
public class Command : IBindable, ICommand
159159
{
160-
#region Events
160+
#region Events
161161

162162
/// <summary>
163163
/// Occurs when the <see cref="Enabled"/> property is changed.
@@ -189,9 +189,9 @@ protected virtual void OnExecuted(EventArgs e)
189189
Executed(this, e);
190190
}
191191

192-
#endregion
192+
#endregion
193193

194-
#region Properties
194+
#region Properties
195195

196196
/// <summary>
197197
/// Gets or sets the ID of the command
@@ -261,7 +261,7 @@ public virtual bool Enabled
261261
/// <value>The command shortcut.</value>
262262
public Keys Shortcut { get; set; }
263263

264-
#endregion
264+
#endregion
265265

266266
/// <summary>
267267
/// Initializes a new instance of the <see cref="Eto.Forms.Command"/> class.
@@ -329,8 +329,8 @@ public static implicit operator ToolItem(Command command)
329329
/// Gets the dictionary of properties for this widget
330330
/// </summary>
331331
public PropertyStore Properties
332-
{
333-
get { return properties ?? (properties = new PropertyStore(this)); }
332+
{
333+
get { return properties ?? (properties = new PropertyStore(this)); }
334334
}
335335

336336
static readonly object Command_Key = new object();
@@ -378,7 +378,7 @@ event EventHandler ICommand.CanExecuteChanged
378378
}
379379

380380
bool ICommand.CanExecute(object parameter)
381-
{
381+
{
382382
return Enabled;
383383
}
384384

@@ -387,7 +387,7 @@ void ICommand.Execute(object parameter)
387387
Execute();
388388
}
389389

390-
#region IBindable implementation
390+
#region IBindable implementation
391391

392392
/// <summary>
393393
/// Event to handle when the <see cref="DataContext"/> has changed
@@ -423,7 +423,7 @@ public IBindable Parent
423423
{
424424
get { return Properties.Get<IBindable>(Parent_Key); }
425425
set
426-
{
426+
{
427427
var old = Parent;
428428
Properties.Set(Parent_Key, value, () =>
429429
{
@@ -432,7 +432,11 @@ public IBindable Parent
432432
if (value != null)
433433
value.DataContextChanged += Value_DataContextChanged;
434434
if (!Properties.ContainsKey(DataContext_Key))
435+
{
436+
IsDataContextChanging = true;
435437
OnDataContextChanged(EventArgs.Empty);
438+
IsDataContextChanging = false;
439+
}
436440
});
437441
}
438442
}
@@ -462,10 +466,36 @@ public object DataContext
462466
return bindable != null ? bindable.DataContext : null;
463467
});
464468
}
465-
set { Properties.Set(DataContext_Key, value, () => OnDataContextChanged(EventArgs.Empty)); }
469+
set
470+
{
471+
if (Properties.TrySet(DataContext_Key, value))
472+
{
473+
IsDataContextChanging = true;
474+
OnDataContextChanged(EventArgs.Empty);
475+
IsDataContextChanging = false;
476+
}
477+
}
478+
}
479+
480+
static readonly object IsDataContextChanging_Key = new object();
481+
482+
/// <summary>
483+
/// Gets a value indicating that the <see cref="DataContext"/> property is changing.
484+
/// </summary>
485+
/// <remarks>
486+
/// This can be used to determine when to allow certain logic during the update of the data context.
487+
///
488+
/// It is used to disable binding setters on the model when the data context changes so that a binding
489+
/// does not cause the view model to be updated when the state hasn't been fully set yet.
490+
/// </remarks>
491+
/// <value><c>true</c> if the DataContext is currently changing, <c>false</c> otherwise.</value>
492+
public bool IsDataContextChanging
493+
{
494+
get => Properties.Get<bool?>(IsDataContextChanging_Key) ?? (Parent as IBindable)?.IsDataContextChanging ?? false;
495+
set => Properties.Set(IsDataContextChanging_Key, value);
466496
}
467497

468-
static readonly object Bindings_Key = new object();
498+
static readonly object Bindings_Key = new object();
469499

470500
/// <summary>
471501
/// Gets the collection of bindings that are attached to this widget
@@ -475,6 +505,6 @@ public BindingCollection Bindings
475505
get { return Properties.Create(Bindings_Key, () => new BindingCollection()); }
476506
}
477507

478-
#endregion
508+
#endregion
479509
}
480510
}

test/Eto.Test/UnitTests/Forms/DataContextTests.cs

+39-1
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ public void DataContextChangeShouldFireForControlInStackLayout()
285285
Shown(form =>
286286
{
287287
c = new Panel();
288-
c.DataContextChanged += (sender, e) =>
288+
c.DataContextChanged += (sender, e) =>
289289
dataContextChanged++;
290290

291291
form.Content = new StackLayout
@@ -400,6 +400,44 @@ public void DataContextWithEqualsShouldSet()
400400
Assert.AreEqual(3, changed);
401401
});
402402
}
403+
404+
class DropDownViewModel
405+
{
406+
public string[] DataSource { get; set; }
407+
408+
public int SelectedIndex { get; set; }
409+
}
410+
411+
[Test]
412+
public void ChangingDataContextShouldNotSetValues() => Shown(
413+
form =>
414+
{
415+
var dropDown = new DropDown();
416+
dropDown.BindDataContext(c => c.DataStore, (DropDownViewModel m) => m.DataSource);
417+
dropDown.BindDataContext(c => c.SelectedIndex, (DropDownViewModel m) => m.SelectedIndex);
418+
return dropDown;
419+
},
420+
dropDown =>
421+
{
422+
var model1 = new DropDownViewModel
423+
{
424+
DataSource = new string[] { "Item 1", "Item 2", "Item 3" },
425+
SelectedIndex = 0
426+
};
427+
dropDown.DataContext = model1;
428+
Assert.AreEqual(0, dropDown.SelectedIndex, "#1");
429+
430+
var model2 = new DropDownViewModel
431+
{
432+
DataSource = new string[] { "Item 4", "Item 5", "Item 6" },
433+
SelectedIndex = 1
434+
};
435+
dropDown.DataContext = model2;
436+
Assert.AreEqual(1, dropDown.SelectedIndex, "#2");
437+
438+
Assert.AreEqual(0, model1.SelectedIndex, "#3 - Model 1 was changed");
439+
Assert.AreEqual(1, model2.SelectedIndex, "#4 - Model 2 was changed");
440+
});
403441
}
404442
}
405443

0 commit comments

Comments
 (0)