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
[API Proposal]: Introduce Commands and DataContext to WinForms to adapt modern Binding scenarios #4895
Comments
I locked this for the time being, since this is still work in progress. |
@merriemcgaw: Talked to @OliaG about this in the context of the last survey, and we'd like to plan this in the .NET 7 timeframe. Unlocking this now. Although we won't start working on this for a while, I am happy for any suggestions folks my have for this! |
Regarding your proposition to make ToolStripItem bindable, see also this discussion about using binding with toolstripcontrolhost. Also, we wanted to use binding in StatusStrip, on ToolStripStatusLabel, and found that we had to wire to the BindingContext of the Form for the binding to work. Maybe this scenario could be supported as well (which I think is certainly common: wanting to show some model information in the StatusStrip of a form). Thanks, |
Yes, this is what I had in mind. Since ToolStripItem is the base for so many things, making that bindable in principle should have the requested result. I'd need to experiment a bit with #6517 and see what's the deal there. |
For our case, we had to make 3 things for ToolStripStatusLabel:
public class BindableStatusStrip : StatusStrip
{
/// <summary>
/// Raises the <see cref="Control.BindingContextChanged"/> event.
/// </summary>
protected override void OnBindingContextChanged(EventArgs e)
{
base.OnBindingContextChanged(e);
foreach (var toolStripItem in Items)
{
if (toolStripItem is IBindableToolStripItem bindableToolStripItem)
{
bindableToolStripItem.UpdateBindingContext();
}
}
}
}
/// <summary>
/// Describe a <see cref="ToolStripItem"/> which implements <see cref="IBindableComponent"/>.
/// </summary>
/// <remarks>
/// <para>
/// This interface must be added to any <see cref="ToolStripItem"/> which wants to implement <see cref="IBindableComponent"/>.
/// It is required because <see cref="ToolStripItem"/> does not inherit from <see cref="Control"/>, and so do not receive notifications
/// when the <see cref="BindingContext"/> change on the parent control.
/// </para>
/// <para>
/// Implementing this interface is required by any class based on <see cref="ToolStripItem"/> and the <see cref="StatusStrip"/> container must be
/// replaced by <see cref="BindableStatusStrip"/>, which track <see cref="Control.OnBindingContextChanged(EventArgs)"/> to call <see cref="UpdateBindingContext"/> on each
/// <see cref="ToolStripItem"/> from <see cref="ToolStrip.Items"/> which implements <see cref="IBindableToolStripItem"/>.
/// </para>
/// </remarks>
public interface IBindableToolStripItem : IBindableComponent
{
/// <summary>
/// Notify this <see cref="ToolStripItem"/> that the parent binding context has changed, and that the bindings exposed by <see cref="IBindableComponent.DataBindings"/> must be updated.
/// </summary>
void UpdateBindingContext();
}
public class ToolStripStatusBindableLabel : ToolStripStatusLabel, IBindableComponent, IBindableToolStripItem
{
private BindingContext _bindingContext;
private ControlBindingsCollection _dataBindings;
/// <summary>
/// Implémentation de <see cref="IBindableComponent.BindingContext"/>.
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
EditorBrowsable(EditorBrowsableState.Advanced)]
public BindingContext BindingContext
{
get
{
if (_bindingContext != null)
return _bindingContext;
var parent = (Control)Owner;
if (parent != null)
return parent.BindingContext;
return null;
}
set
{
_bindingContext = value;
}
}
/// <summary>
/// Implémentation de <see cref="IBindableComponent.DataBindings"/>.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[RefreshProperties(RefreshProperties.All)]
[ParenthesizePropertyName(true)]
public ControlBindingsCollection DataBindings
{
get { return this._dataBindings ?? (this._dataBindings = new ControlBindingsCollection(this)); }
}
/// <summary>
/// Libère les ressources associées à ce ToolStrip<see cref="ToolStripFormattedLabel"/>.
/// </summary>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
this.Events.Dispose();
if (this._dataBindings != null)
{
this._dataBindings.Clear();
this._dataBindings = null;
}
}
}
protected override void OnParentChanged(ToolStrip oldParent, ToolStrip newParent)
{
base.OnParentChanged(oldParent, newParent);
}
public void UpdateBindingContext()
{
if (_dataBindings != null)
{
for (int i = 0; i < _dataBindings.Count; i++)
{
BindingContext.UpdateBinding(BindingContext, _dataBindings[i]);
}
}
}
} |
@KlausLoeffelmann In your Devblogs article Databinding with the OOP Windows Forms Designer you promised to introduce some sample code in context of this issue. Almost a month has passed since February 2 - where we can find some basic tutorial for modern WinForms databinding? I've only found outdated docs for WinForms databinding with EF6 :( |
We're still working on the blogs. It's taking a bit longer than anticipated but it's actively in progress. |
@merriemcgaw @KlausLoeffelmann Looking forward for some documentation to come soon. Without clear explanation it's not easy to understand new Command approach in WinForms. |
I know folks are waiting for more demos and concepts how to implement this in real life demos. To be as transparent as possible: Our SDK, improving the usage of the SDK, and - of course - its documentation has however higher priority, so we need to complete a few work items there, first. To keep this discussion alive and give folks a clearer idea how this functionality can be used (and also to address the more and more incoming requests for a model-binding demo from the Databinding post, and also as an answer to @bairog, here is a link to my private repo, in which I was collecting ideas around this. It's pretty much what the Databinding posts also reflects. Also please, keep in mind, the primary idea of the Databinding post was to introduce the new Databinding functionality in the designer. We should lead the discussion about WinForms UI Controllers/ViewModel ideas here. |
I have played just a few minutes with your code. Did you already think about having a transient DataContext? Where the parent datacontext is by default visible to all contained controls? It's a concept well-known in WPF and it has its advantages but also drawbacks. I am not sure it will fit winforms, basically because the BindingSource is still the way to go and already requires a to add a component on each usercontrol. And also because to make it work it will certainly requires to implement IDataContextTarget at the Control level, so any child control could receive the parent datacontext (or instead return the parent value if not overridden). In the end, I am not sure I would suggest adding DataContext to the base Control class. I think I will only suggest implementing it in Form and UserControl class, and let developers share the parent DataContext with children. We have a lot of big forms where we have a master "datacontext". We split our UI in different usercontrols for practical reasons (performances and maintenability) and we pass our "datacontext" from the parent form to each control. |
Also I did not understand what is the goal of the BindableCommandComponent. The ToUpper menu did nothing on your example and the bindableCommandComponent was bind to this command. PS: Feel free to ignore my messages if your are focused on other things. I suppose everything will be more clear when you will post in the blog about this topic. |
Yes, that was my plan. Actually, if we implement that, I still want to do it in an Ambient Property-ish way, but probably limit it to |
Yeah, I always was ambivalent about that as well, and wrote "Optional" to begin with. I am gonna drop this, it makes no sense. Or not enough sense. |
namespace System.Windows.Forms
{
public class CommandControl : Control
{
event EventHandler? CommandChanged;
event EventHandler? CommandCanExecuteChanged;
/// <summary>
/// Gets or sets the Command to invoke, when the implementing
/// Component or Control is triggered by the user.
/// </summary>
System.Windows.Input.ICommand? Command { get; set; }
/// <summary>
/// Gets or sets the CommandParameter for the Component or Control.
/// </summary>
object? CommandParameter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the control
/// can respond to user interaction.
/// </summary>
bool Enabled { get; set; }
// Needed to restore the previous Enabled status
// when a new Command gets assigned.
protected bool? PreviousEnabledStatus { get; set; }
// Needed to call OnCommandChanged to meet
// WinForms property implementation conventions for Data Binding.
protected void RaiseCommandChanged(EventArgs e);
// Needed to call OnCommandCanExecuteChanged to meet
// WinForms property implementation conventions for Data Binding.
protected void RaiseCommandCanExecuteChanged(object sender, EventArgs e);
// Needed to call OnCommandExecuting to meet
// WinForms property implementation conventions for Data Binding.
protected void RaiseCommandExecuting(CancelEventArgs e);
/// <summary>
/// Handles the assignment of a new Command to the Command property.
/// </summary>
/// <param name="commandComponent">Instance of the component which
/// provides the Command property.</param>
/// <param name="newCommand">New Command value to be assign.</param>
/// <param name="commandBackingField">The backing field of the component
/// which holds the command for the property.</param>
protected static void CommandSetter(
ICommandProvider commandComponent,
ICommand newCommand,
ref ICommand commandBackingField)
{ }
/// <summary>
/// Handels the invoke of the command, e.g. called by a button's click event.
/// </summary>
/// <param name="commandComponent"></param>
protected static void RequestCommandExecute(ICommandProvider commandComponent) { }
}
} namespace System.Windows.Forms
{
- public abstract partial class ButtonBase : Control
+ public abstract partial class ButtonBase : CommandControl
} |
Challenges we face when we put this under Preview by attributing respective new APIs with
It's different though with binding to a component which has derived from What we're currently exploring to mitigate this is to introduce a (Designer-side) shadow property 'DataBindings' which the Designer examines on CodeDom-serialization. And if we find a binding definition to a property which is attributed with |
namespace System.Windows.Forms;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public abstract class BindableComponent : Component, IBindableComponent
{
public event EventHandler? BindingContextChanged;
public ControlBindingsCollection? DataBindings { get; }
public BindingContext? BindingContext { get; set; }
protected virtual void OnBindingContextChanged(EventArgs e);
}
// Suppress warning
public abstract class Control : BindableComponent // Used to be Component
{
}
public partial class ButtonBase
{
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandCanExecuteChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandParameterChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public ICommand? Command { get; set; }
[RequiresPreviewFeatures(Url = "<placeholder>")]
public object? CommandParameter { get; set; }
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandCanExecuteChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandParameterChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnRequestCommandExecute(EventArgs e);
}
public abstract partial class ToolStripItem : BindableComponent // Used to be Component
{
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandCanExecuteChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public event EventHandler? CommandParameterChanged;
[RequiresPreviewFeatures(Url = "<placeholder>")]
public ICommand? Command { get; set; }
[RequiresPreviewFeatures(Url = "<placeholder>")]
public object? CommandParameter { get; set; }
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandCanExecuteChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnCommandParameterChanged(EventArgs e);
[RequiresPreviewFeatures(Url = "<placeholder>")]
protected virtual void OnRequestCommandExecute(EventArgs e);
} |
I know it will certainly not be feasible, because the CodeDom serialization code for the designer must be a really dangerous place to look at :D, but if the string is replaced with a this._myButton.DataBindings.Add(new System.WindowsForms.Binding(nameof(this._myButton.Command), ...)); |
This (and many other improvements) will be possible once the designer code DOM generation is migrated to the Roslyn engine. This is work in progress, but no definite timelines. |
Rationale:
We want our customers to modernize their existing WinForms Apps and allow them to adapt new Azure-based technologies. It is important for our customers, that the business logic of their WinForms LOB apps to this end becomes more flexible and can be rearchitected away from the WinForms-typical code-behind and towards UI Controller or ViewModels, which can be easy reused and unit tested. This is also an incentive to pivot away from the 15 years old Typed DataSets in WinForms, which have very limited support in .NET WinForms Apps over .NET Framework, since the necessary Data Source Provider Service is not available for .NET Apps altogether. Feedback for suggesting this as our new Binding strategy was highly engaged, very positive, and shows that we're on the right way with that.
WinForms has a pretty powerful binding engine, but to achieve those things, a few essential features are missing:
System.Windows.Input.ICommand
.DataContext
property does that in WPF (orBindingContext
in Maui Apps).Having these features in WinForms, enables a series of important scenarios:
Easier adaptation of modern ORMs: Since for the .NET Designer, the Data Source Provider Service is not available, with this new feature, it becomes easier to adapt and migrate away from ancient WinForms data layer architectures: While typed
DataSet
andDataTable
were clearly designed with the WinForms code-behind model and direct binding to UI Elements of WinForms in mind, with the new data binding features present, modernized WinForms Apps could more easily consume typical ViewModels and Models as their Controller and Data Layer abstractions, and, what's important: Models in this scenarios being POCO classes which are generated and maintained by Entity Framework or Entity Framework Core.Making it possible to unit test WinForms Apps business logic: Especially with the introduction of commands and command parameters, it would be way easier to use ViewModels/UI-Controller in binding scenarios for WinForms, that can be reused in Maui scenarios. While WinForms will not be able to achieve the same flexibility as compared to XAML based data binding, binding commands is a considerable step in that direction, Customers have repeatedly asked for that, and making ToolStripItems bindable at the same time, would be equally helpful in easier adapting ViewModels/UI Controller Models.
It's almost impossible to unit test Code-behind apps, which mixes business logic and UI-Control code. But that's how WinForms Apps are typically "architected". The new API makes it way simple, to separated the Business Logic from the actual Code-Behind code, so it can be properly unit and integration tested.
Github Repo for Sample-App
https://github.com/KlausLoeffelmann/WinFormsCommandBinding
API-Changes, Overview:
Note: For .NET 7, we want to put this feature out in Preview, attributing respective new classes and properties with the
RequiresPreviewFeature
attribute, collect feedback and then enable it for .NET 8.To provide a more ViewModel/View Controller compatible way for a WinForms LOB architecture, I propose the following API-Changes:
Introduce the abstract classes
BindableComponent
,CommandComponent
andCommandControl
which describes the necessary methods and properties for extending a WinForms Component or a WinForms Control with the command logic. Since a Component is not bindable by default, we introduce a new abstract class which is bringing all the necessary infrastructure to make a Component bindable. CommandComponent (literally) on top of that introduces aCommand
property, allows to executes anAction
assigned toCommand
, when the context allows for the Command to be executed. The action assigned toCommand
is being passed the parameter which is stored in the Component's or Control's new propertyCommandParameter
. This is the same way, commands work in WPF, UWP, Xamarin and Maui.Inherit
ButtonBase
fromCommandControl
instead ofControl
, so it will get all the necessary infrastructure for binding to commands, and for executing commands - if the context allows -OnClick
.Inherit
ToolStripItem
forCommandComponent
, to make a) allToolStripItem
derived components bindable, and b) introduce a bindableCommand
property on allToolStripItem
derived components which can be clicked. Note:IBindableComponent
is not a new API. It was especially developed to provide Data Binding for components (and it's of course implemented inControl
to make it bindable). Implementing this Interface makes the new APIsBindingContext
andOnBindingContextChanged
necessary. The functionality ofBindingContext
is handed over to the existing static methodBindingContext.UpdateBindings
, which was especially designed to handle bindable Components. The idea is to seperateBindableComponent
andCommandComponent
, so other components can derive fromBindableComponent
to provide principle binding functionality, without necessarily to provide command binding.Introduce a property
DataContext
onControl
as an ambient property, and - to meet the WinForms DataBinding conventions - implement a virtual protected methodOnDataContextChanged
and theDataContextChanged
event along with it. This way a child (User)Control can marshal a new/changing data source to whateverBindingSourceComponent
needs to be assigned to.FAQ
A few of most frequently asked questions up to now:
Why do we need
On[Propertyname]Changed
methods and[Propertyname]Changed Events
for each propertyThat's the convention in WinForms to support data binding back to the source: A property is bindable in the direction to the data source only, if the property can raise a respective
xxxChanged
event. A further convention in WinForms is that for each event there is aOnxxx
method, that can be overwritten in a derived class, which raises the event, since events cannot be raised directly in a derived class.What's the reason for the naming
Naming pf new properties is based on precedence in existing UI stacks (
Command
,CommandParameter
,DataContext
in WPF). Naming of additional new events and methods is based on either existing interfaces or naming conventions in WinForms.Why are we only introducing Commands on
ButtonBase
andToolStripItem
Because only those make sense in the context of executing commands, and that's the way is done also in WPF. We have been discussing LinkLabel, but came to the conclusion that it would probably too special a use case. Since we are providing an easy way to extend controls with commands by letting exsting Controls or Commands inherit from
CommandComponent
orCommandControl
, users can easily implement additional scenarios in controls how they see fit. The same is true for control vendors.Do we need to extend the Designer?
No. As can be seen in the screenshots here, the Designer can deal with every of these changes out of the box. There is no necessity for additional work in the WinForms Designer.
If we have a typical Controller or ViewModel based on a MVVM-Framework like the MVVM Toolkit (or just implementing
INotifyPropertyChanged
directly like in the following sample), then the Designer can already work with that (see following screenshot):List of new APIs
Introducing the new abstract component
BindableComponent
Note: Even though on first glance this might sense to put in the
System.Component
namespace and in the runtime, this is for the WinForms binding infrastructure only:IBindableComponent
is inSystem.Windows.Forms
and so isBindingContext
.Providing the infrastructure for Updating/Managing binding for Components has been implemented in WinForms all along, see here: BindingContext.cs (dot.net)
Introducing the new abstract component
CommandComponent
Introducing the new abstract control
CommandControl
Inheriting
ButtonBase
fromCommandControl
Inheriting
ToolStripItem
fromCommandComponent
Introducing
DataContext
on the existing classclass Control
We need an extension point for assigning the data sources not only to a
Form
or aUserControl
, but at the same time make that data source available to each of the child (User)Controls which this root control is hosting. To this end, we introduce theDataContext
Property of typeobject
onControl
, and we implement it as an ambient property. This way, it doesn't use any additional memory resources, as long as it is not used. When it is assigned to a Form, itsOnDataContextChanged
for the Form (and for each respective child control) is called.OnDataContextChanged
then raises theDataContextChanged
event. The root control or/and the children can handle that event to decide, what to do with the data source: Usually, the data source then gets assigned to theBindingSource
component(s) the Control uses, which then initiates the actual data binding.The text was updated successfully, but these errors were encountered: