From b22fe0a7823b343db7d584633c929abf5c188f10 Mon Sep 17 00:00:00 2001 From: "everLEEst(SangHyeon Lee)" Date: Wed, 12 Feb 2025 15:57:15 +0900 Subject: [PATCH] [NUI] Introduce new binding style. The binding in NUI cause performance and memory issue, so we plan to deprecate them by setting IsUsingXaml disable. This patch is aim to introduce new style of binding, which do not causing performance and memory seriously, and user can add binding property on existing view class, so that we do not need to add every property to bindable. --- .../Item/RecyclerViewItem.Internal.cs | 1 + .../src/devel/Binding/BindingExtensions.cs | 207 ++++++++++++++++++ .../src/devel/Binding/BindingProperty.cs | 19 ++ .../src/devel/Binding/BindingSession.cs | 193 ++++++++++++++++ .../devel/Binding/TwoWayBindingProperty.cs | 29 +++ .../src/devel/Binding/ViewBindings.cs | 193 ++++++++++++++++ .../Tizen.NUI.StyleGuide.cs | 8 +- 7 files changed, 649 insertions(+), 1 deletion(-) create mode 100644 src/Tizen.NUI/src/devel/Binding/BindingExtensions.cs create mode 100644 src/Tizen.NUI/src/devel/Binding/BindingProperty.cs create mode 100644 src/Tizen.NUI/src/devel/Binding/BindingSession.cs create mode 100644 src/Tizen.NUI/src/devel/Binding/TwoWayBindingProperty.cs create mode 100644 src/Tizen.NUI/src/devel/Binding/ViewBindings.cs diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs index 7808987ba31..54285980c8d 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs @@ -227,6 +227,7 @@ protected override void OnEnabled(bool enabled) protected override void OnBindingContextChanged() { PropagateBindingContext(this); + base.OnBindingContextChanged(); } private void PropagateBindingContext(View parent) diff --git a/src/Tizen.NUI/src/devel/Binding/BindingExtensions.cs b/src/Tizen.NUI/src/devel/Binding/BindingExtensions.cs new file mode 100644 index 00000000000..8764109b279 --- /dev/null +++ b/src/Tizen.NUI/src/devel/Binding/BindingExtensions.cs @@ -0,0 +1,207 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using Tizen.NUI.BaseComponents; + +namespace Tizen.NUI.Binding +{ + /// + /// Provides extension methods for binding properties of a view model to a view. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class BindingExtensions + { + /// + /// Sets the binding for the specified view model property. + /// + /// The type of the view model. + /// The type of the view. + /// The view to bind to. + /// The view model to bind from. + /// The action to set the view property. + /// The path of the view model property. + /// The view. + public static TView SetBinding(this TView view, BindingSession vm, Action set, string path) where TView : View + { + _ = view ?? throw new ArgumentNullException(nameof(view)); + + var setter = new Action(vm => + { + set.Invoke(vm, view); + }); + vm.AddBinding(setter, path); + return view; + } + + /// + /// Sets the binding for the specified view model property. + /// + /// The type of the view model. + /// The view to bind to. + /// The view model to bind from. + /// The action to set the view property. + /// The path of the view model property. + /// The view. + public static View SetBinding(this View view, BindingSession vm, Action set, string path) + { + vm.AddBinding(set, path); + return view; + } + + /// + /// Sets the binding for the specified view model property. + /// + /// The type of the view model. + /// The view to bind to. + /// The binding session. + /// The path of the view property. + /// The path of the view model property. + /// The view. + public static View SetBinding(this View view, BindingSession session, string targetPath, string srcPath) + { + var setter = new Action(model => + { + if (view.Disposed) + { + return; + } + var prop = view.GetType().GetProperty(targetPath, BindingFlags.Public | BindingFlags.Instance); + prop.SetValue(view, session.GetValue(srcPath)); + }); + session.AddBinding(setter, srcPath); + return view; + } + + /// + /// Sets the binding for the specified view model property. + /// + /// The type of the view. + /// The type of the view model. + /// The type of the view property. + /// The view to bind to. + /// The binding session. + /// The view property. + /// The path of the view model property. + /// The view. + public static TView SetBinding(this TView view, BindingSession session, BindingProperty property, string path) where TView : View + { + var setter = new Action(model => + { + if (view.Disposed) + { + return; + } + + var value = session.GetValue(path); + // need to apply convertor if type was not mached + if (value != null && !typeof(TProperty).IsAssignableFrom(value.GetType())) + { + // only string type convert with ToString() + if (typeof(TProperty) == typeof(string)) + { + value = value.ToString(); + } + else + { + throw new InvalidCastException($"Cannot cast {value.GetType()} to {typeof(TProperty)}"); + } + } + property.Setter(view, (TProperty)value); + }); + session.AddBinding(setter, path); + return view; + } + + /// + /// Sets the two-way binding for the specified view model property. + /// + /// The type of the view model. + /// The type of the view property. + /// The view to bind to. + /// The binding session. + /// The view property. + /// The path of the view model property. + /// The view. + public static View SetTwoWayBinding(this View view, BindingSession session, TwoWayBindingProperty property, string path) + { + var regit = new Action(act => + { + property.AddObserver(view, act); + }); + var unregit = new Action(act => + { + property.RemoveObserver(view, act); + }); + var setter = new Action(model => + { + if (view.Disposed) + { + return; + } + property.Setter(view, (TProperty)session.GetValue(path)); + }); + var getter = new Func(() => property.Getter(view)); + session.AddTwoWayBinding(regit, unregit, setter, getter, path); + return view; + } + + /// + /// Sets the two-way binding for the specified view model property. + /// + /// The type of the view. + /// The type of the view model. + /// The type of the view property. + /// The to. + /// The binding session. + /// The view property. + /// The path of the view model property. + /// The view. + public static TView SetTwoWayBinding(this TView view, BindingSession session, TwoWayBindingProperty property, string path) where TView : View + { + var regit = new Action(act => + { + property.AddObserver(view, act); + }); + var unregit = new Action(act => + { + property.RemoveObserver(view, act); + }); + var setter = new Action(model => + { + if (view.Disposed) + { + return; + } + property.Setter(view, (TProperty)session.GetValue(path)); + }); + var getter = new Func(() => property.Getter(view)); + session.AddTwoWayBinding(regit, unregit, setter, getter, path); + return view; + } + + /// + /// Gets the binding session for the specified view. + /// + /// The type of the view model. + /// The view. + /// The binding session. + public static BindingSession BindingSession(this View view) + { + return view.GetAttached>(); + } + + /// + /// Sets the binding session for the specified view. + /// + /// The type of the view model. + /// The type of the view. + /// The view. + /// The binding session. + /// The view. + public static T BindingSession(this T view, BindingSession session) where T : View + { + view.SetAttached(session); + return view; + } + } +} \ No newline at end of file diff --git a/src/Tizen.NUI/src/devel/Binding/BindingProperty.cs b/src/Tizen.NUI/src/devel/Binding/BindingProperty.cs new file mode 100644 index 00000000000..fba9b374f6e --- /dev/null +++ b/src/Tizen.NUI/src/devel/Binding/BindingProperty.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel; + +namespace Tizen.NUI.Binding +{ + /// + /// The BindingProperty class represents a binding property for a view. + /// + /// The type of the view. + /// The type of the value. + [EditorBrowsable(EditorBrowsableState.Never)] + public class BindingProperty + { + /// + /// Gets or sets the setter action for the binding property. + /// + public Action Setter { get; set; } + } +} diff --git a/src/Tizen.NUI/src/devel/Binding/BindingSession.cs b/src/Tizen.NUI/src/devel/Binding/BindingSession.cs new file mode 100644 index 00000000000..4c4c4cf78c1 --- /dev/null +++ b/src/Tizen.NUI/src/devel/Binding/BindingSession.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using Tizen.NUI.BaseComponents; + +namespace Tizen.NUI.Binding +{ + /// + /// BindingSession class provides a mechanism for binding properties of a view model to a view. + /// + /// The type of the view model. + [EditorBrowsable(EditorBrowsableState.Never)] + public class BindingSession : INotifyPropertyChanged, IDisposable + { + private List _bindingActions = new List(); + private List _changedAction = new List(); + private Dictionary> _observers = new Dictionary>(); + private TViewModel _viewmodel; + private bool _disposed; + + /// + /// Represents an event that is raised when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Gets or sets the view model. + /// + public TViewModel ViewModel + { + get => _viewmodel; + set + { + if (_viewmodel != null && _viewmodel is INotifyPropertyChanged noti) + { + noti.PropertyChanged -= OnPropertyChanged; + } + _viewmodel = value; + + if (_viewmodel != null && _viewmodel is INotifyPropertyChanged newobj) + { + newobj.PropertyChanged += OnPropertyChanged; + } + + UpdateViewModel(); + } + } + + /// + /// Updates the view model. + /// + /// The view model to update. + public void UpdateViewModel(TViewModel vm) + { + ViewModel = vm; + } + + /// + /// Updates the view model. + /// + public void UpdateViewModel() + { + foreach (var action in _bindingActions) + { + action.Invoke(); + } + } + + /// + /// Adds a binding between a property of the view model and a property of the view. + /// + /// The setter method of the view. + /// The path of the property to bind. + public void AddBinding(Action setter, string path) + { + var action = new Action(() => + { + if (ViewModel != null) + setter(ViewModel); + }); + _bindingActions.Add(action); + PropertyChangedEventHandler handler = (s, e) => + { + if (path == "*" || e.PropertyName == path || e.PropertyName == "*") + { + action(); + } + }; + _changedAction.Add(handler); + PropertyChanged += handler; + action.Invoke(); + } + + /// + /// Adds a two-way binding between a property of the view model and a property of the view. + /// + /// The type of the property to bind. + /// The registration method of the observer. + /// The unregistration method of the observer. + /// The setter method of the view. + /// The getter method of the view. + /// The path of the property to bind. + public void AddTwoWayBinding(Action register, Action unregister, Action setter, Func getter, string path) + { + var action = new Action(() => + { + if (ViewModel != null) + SetValue(getter(), path); + }); + _observers[action] = unregister; + register(action); + AddBinding(setter, path); + } + + /// + /// Clears all bindings. + /// + public void ClearBinding() + { + foreach (var evtHandler in _changedAction) + { + PropertyChanged -= evtHandler; + } + foreach (var observer in _observers) + { + observer.Value.Invoke(observer.Key); + } + _observers.Clear(); + _changedAction.Clear(); + _bindingActions.Clear(); + } + + /// + /// Gets the value of a property of the view model. + /// + /// The name of the property. + /// The value of the property. + public object GetValue(string name) + { + if (name == ".") + return ViewModel; + + var prop = ViewModel.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); + if (prop == null) + { + Log.Error("NUI", $"Binding : Property {name} not found"); + } + return prop?.GetValue(ViewModel) ?? null; + } + + /// + /// Sets the value of a property of the view model. + /// + /// The value to set. + /// The name of the property. + public void SetValue(object obj, string name) + { + var prop = ViewModel.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); + prop?.SetValue(ViewModel, obj); + } + + /// + /// Releases all resources used by the current instance of the BindingSession class. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Provides a mechanism for releasing unmanaged resources used by the BindingSession class. + /// + /// True if the method is called from Dispose, false if it is called from the finalizer. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + ClearBinding(); + } + _disposed = true; + } + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + } +} \ No newline at end of file diff --git a/src/Tizen.NUI/src/devel/Binding/TwoWayBindingProperty.cs b/src/Tizen.NUI/src/devel/Binding/TwoWayBindingProperty.cs new file mode 100644 index 00000000000..c1c3d3a9cc7 --- /dev/null +++ b/src/Tizen.NUI/src/devel/Binding/TwoWayBindingProperty.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; + +namespace Tizen.NUI.Binding +{ + /// + /// This class represents a two-way binding property between a view and its value. + /// + /// The type of the view. + /// The type of the value. + [EditorBrowsable(EditorBrowsableState.Never)] + public class TwoWayBindingProperty : BindingProperty + { + /// + /// Gets or sets the function that retrieves the value from the view. + /// + public Func Getter { get; set; } + + /// + /// Gets or sets the action that adds an observer to the view. + /// + public Action AddObserver { get; set; } + + /// + /// Gets or sets the action that removes an observer from the view. + /// + public Action RemoveObserver { get; set; } + } +} diff --git a/src/Tizen.NUI/src/devel/Binding/ViewBindings.cs b/src/Tizen.NUI/src/devel/Binding/ViewBindings.cs new file mode 100644 index 00000000000..fa57b7c86e3 --- /dev/null +++ b/src/Tizen.NUI/src/devel/Binding/ViewBindings.cs @@ -0,0 +1,193 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using Tizen.NUI.BaseComponents; + +namespace Tizen.NUI.Binding +{ + /// + /// Provides a set of static properties that represent the binding properties of the class. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class ViewBindings + { + /// + /// Gets the binding property for the width of a . + /// + public static BindingProperty WidthProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.SizeWidth = value, + }; + + /// + /// Gets the binding property for the height of a . + /// + public static BindingProperty HeightProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.SizeHeight = value, + }; + + /// + /// Gets the binding property for the background color of a . + /// + public static BindingProperty BackgroundColorProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.SetBackgroundColor(value), + }; + } + + /// + /// Provides a set of static properties that represent the data-binding capabilities of the class. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TextLabelBindings + { + /// + /// Gets the binding property for the property. + /// + public static BindingProperty TextProperty { get; } = new BindingProperty + { + Setter = (v, value) => + { + v.Text = value; + } + }; + + /// + /// Gets the binding property for the property. + /// + public static BindingProperty TextColorProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.TextColor = value.ToReferenceType(), + }; + + /// + /// Gets the binding property for the property. + /// + public static BindingProperty FontSizeProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.PointSize = value, + }; + } + + /// + /// This class provides a set of static properties for binding with control. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TextFieldBindings + { + /// + /// The TextColorProperty is a bindable property that indicates the color of the text in the control. + /// + public static BindingProperty TextColorProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.TextColor = value.ToReferenceType(), + }; + + /// + /// The FontSizeProperty is a bindable property that indicates the size of the font used to display the text in the control. + /// + public static BindingProperty FontSizeProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.PointSize = value, + }; + + /// + /// The TextProperty is a two-way bindable property that indicates the text displayed in the control. + /// + public static TwoWayBindingProperty TextProperty { get; } = new TwoWayBindingProperty + { + Setter = (v, value) => v.Text = value, + Getter = v => v.Text, + AddObserver = (v, action) => v.TextChanged += EventHandlerHelper.Set>((s, e) => { action.Invoke(); }, action), + RemoveObserver = (v, action) => v.TextChanged -= EventHandlerHelper.Get>(action), + }; + } + + /// + /// Provides a set of static properties for binding TextEditor controls. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TextEditorBindings + { + /// + /// The TextColorProperty is a bindable property that indicates the color of the text in the TextEditor control. + /// + public static BindingProperty TextColorProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.TextColor = value.ToReferenceType(), + }; + + /// + /// The FontSizeProperty is a bindable property that indicates the size of the font used to display the text in the TextEditor control. + /// + public static BindingProperty FontSizeProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.PointSize = value, + }; + + /// + /// The TextProperty is a two-way bindable property that indicates the text displayed in the TextEditor control. + /// + public static TwoWayBindingProperty TextProperty { get; } = new TwoWayBindingProperty + { + Setter = (v, value) => v.Text = value, + Getter = v => v.Text, + AddObserver = (v, action) => v.TextChanged += EventHandlerHelper.Set>((s, e) => { action.Invoke(); }, action), + RemoveObserver = (v, action) => v.TextChanged -= EventHandlerHelper.Get>(action), + }; + } + + /// + /// Provides a set of static properties that represent the bindable properties of the class. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class ImageViewBindings + { + /// + /// Represents the bindable property for the property. + /// + public static BindingProperty ResourceUrlProperty { get; } = new BindingProperty + { + Setter = (v, value) => v.ResourceUrl = value, + }; + } + + /// + /// EventHandlerHelper class provides a helper method to set and get event handlers using actions. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class EventHandlerHelper + { + private static Dictionary s_actionHandlerMap = new Dictionary(); + + /// + /// Sets the event handler for the given action. + /// + /// The type of the event handler. + /// The event handler to set. + /// The action to associate with the event handler. + /// The event handler that was set. + public static TEventHandler Set(TEventHandler handler, Action action) where TEventHandler : Delegate + { + s_actionHandlerMap[action] = handler; + return handler; + } + + /// + /// Gets the event handler associated with the given action and removes the association. + /// + /// The type of the event handler. + /// The action to get the event handler for. + /// The event handler associated with the given action, or null if no association exists. + public static TEventHandler Get(Action action) where TEventHandler : Delegate + { + if (!s_actionHandlerMap.ContainsKey(action)) + return null; + + var handler = (TEventHandler)s_actionHandlerMap[action]; + s_actionHandlerMap.Remove(action); + return handler; + } + } +} diff --git a/test/Tizen.NUI.StyleGuide/Tizen.NUI.StyleGuide.cs b/test/Tizen.NUI.StyleGuide/Tizen.NUI.StyleGuide.cs index da74df0e398..51e0615fe8e 100644 --- a/test/Tizen.NUI.StyleGuide/Tizen.NUI.StyleGuide.cs +++ b/test/Tizen.NUI.StyleGuide/Tizen.NUI.StyleGuide.cs @@ -22,6 +22,7 @@ using Tizen.NUI.Components; using Tizen.NUI.BaseComponents; using Tizen.NUI.Binding; +//using Tizen.NUI.Bindings; using System.Reflection; namespace Tizen.NUI.StyleGuide @@ -307,11 +308,16 @@ private void SetMainPage() ItemsLayouter = new LinearLayouter(), ItemTemplate = new DataTemplate(() => { + var session = new BindingSession(); DefaultLinearItem item = new DefaultLinearItem() { WidthSpecification = LayoutParamPolicies.MatchParent, }; - item.Label.SetBinding(TextLabel.TextProperty, "ViewLabel"); + item.BindingContextChanged += (sender, e) => + { + session.ViewModel = (ControlMenu)item.BindingContext; + }; + item.Label.SetBinding(session, TextLabelBindings.TextProperty, "ViewLabel"); item.Label.HorizontalAlignment = HorizontalAlignment.Begin; item.Focusable = true; //BaseComponents' Focusable is false as a default value, true should be set to navigate key focus. return item;