diff --git a/README.md b/README.md index ac16635..dad7b2f 100644 --- a/README.md +++ b/README.md @@ -840,6 +840,44 @@ The `BindableButton` can be bound to the following commands: To pass a parameter to the viewmodel, see the [ParameterValueConverter](#parametervalueconverterttargettype) section. +#### BindableDropdownField + +The `BindableDropdownField` allows you to work with dropdown. To set the binding of the selected value, you need to write `binding-selected-item-path`, and to set the binding of all dropdown elements, you need to use `binding-items-source-path`. +Moreover, you can set them independently of each other. + +The following example demonstrates how to bind to a collection of strings with `BindableDropdownField`. + +In XML, you need to write the following: + +```xml + + + +``` +And in the C# class the following: + +```csharp +public class DropdownFieldViewModel : IBindingContext +{ + public DropdownFieldViewModel() + { + TextValues = new ReadOnlyProperty>(new ObservableCollection() + { + "Value1", + "Value2", + "Value3" + }); + + SelectedValue = new Property(); + SelectedValue.Value = "Value1"; + } + + public IProperty SelectedValue { get; } + + public IReadOnlyProperty> Choices { get; } +} +``` + #### BindableListView The `BindableListView` control is the most efficient way to create lists. It uses virtualization and creates VisualElements only for visible items. Use the `binding-items-source-path` of the `BindableListView` to bind to an `ObservableCollection`. diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs new file mode 100644 index 0000000..3f0da78 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs @@ -0,0 +1,127 @@ +#if UNITYMVVMTOOLKIT_TEXTMESHPRO_SUPPORT + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Runtime.CompilerServices; +using TMPro; +using UnityEngine; +using UnityMvvmToolkit.Core; +using UnityMvvmToolkit.Core.Extensions; +using UnityMvvmToolkit.Core.Interfaces; + +namespace UnityMvvmToolkit.UGUI.BindableUGUIElements +{ + [RequireComponent(typeof(TMP_Dropdown))] + public class BindableDropdown : MonoBehaviour, IBindableElement + { + [SerializeField] private TMP_Dropdown _dropdown; + [SerializeField] private string _bindingSelectedItemPath; + [SerializeField] private string _bindingItemsSourcePath; + + private IProperty _selectedItemProperty; + private IReadOnlyProperty> _itemsSource; + + private PropertyBindingData _selectedItemBindingData; + private PropertyBindingData _itemsSourceBindingData; + + public void SetBindingContext(IBindingContext context, IObjectProvider objectProvider) + { + if (string.IsNullOrWhiteSpace(_bindingItemsSourcePath) == false) + { + _itemsSourceBindingData ??= _bindingItemsSourcePath.ToPropertyBindingData(); + _itemsSource = objectProvider + .RentReadOnlyProperty>(context, _itemsSourceBindingData); + _itemsSource.Value.CollectionChanged += OnItemsCollectionChanged; + _dropdown.options = new List(_itemsSource.Value.Select(value => new TMP_Dropdown.OptionData(value))); + } + + if (string.IsNullOrWhiteSpace(_bindingSelectedItemPath) == false) + { + _selectedItemBindingData ??= _bindingSelectedItemPath.ToPropertyBindingData(); + _selectedItemProperty = objectProvider.RentProperty(context, _selectedItemBindingData); + _selectedItemProperty.ValueChanged += OnPropertySelectedItemChanged; + + var foundIndex = _dropdown.options.FindIndex(option => option.text == _selectedItemProperty.Value); + if (foundIndex != -1) + { + UpdateControlValue(foundIndex); + } + + _dropdown.onValueChanged.AddListener(OnControlValueChanged); + _selectedItemProperty.Value = _dropdown.options.Count > 0 ? _dropdown.options[0].text : default; + } + } + + private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (string newItem in e.NewItems) + { + _dropdown.options.Add(new TMP_Dropdown.OptionData(newItem)); + } + } + + if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (string oldItem in e.OldItems) + { + _dropdown.options.Remove(new TMP_Dropdown.OptionData(oldItem)); + } + } + + if (e.Action == NotifyCollectionChangedAction.Reset) + { + _dropdown.options.Clear(); + } + } + + public virtual void ResetBindingContext(IObjectProvider objectProvider) + { + if (_itemsSource != null) + { + _itemsSource.Value.CollectionChanged -= OnItemsCollectionChanged; + objectProvider.ReturnReadOnlyProperty(_itemsSource); + _itemsSource = null; + _dropdown.options = new List(); + } + + if (_selectedItemProperty != null) + { + _selectedItemProperty.ValueChanged -= OnPropertySelectedItemChanged; + objectProvider.ReturnProperty(_selectedItemProperty); + _selectedItemProperty = null; + _dropdown.onValueChanged.RemoveListener(OnControlValueChanged); + } + + UpdateControlValue(default); + } + + protected virtual void OnControlValueChanged(int index) + { + _selectedItemProperty.Value = _dropdown.options[index].text; + } + + private void OnPropertySelectedItemChanged(object sender, string newValue) + { + var foundIndex = _dropdown.options.FindIndex(option => option.text == newValue); + if (foundIndex == -1) + { + return; + } + + UpdateControlValue(foundIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void UpdateControlValue(int newValue) + { + _dropdown.SetValueWithoutNotify(newValue); + } + } +} + +#endif \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs.meta new file mode 100644 index 0000000..05c1cb9 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UGUI/BindableUGUIElements/BindableDropdown.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7044586e66117e48948637bf2c66eaa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs new file mode 100644 index 0000000..cb05421 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Runtime.CompilerServices; +using UnityEngine.UIElements; +using UnityMvvmToolkit.Common.Interfaces; +using UnityMvvmToolkit.Core; +using UnityMvvmToolkit.Core.Extensions; +using UnityMvvmToolkit.Core.Interfaces; + +namespace UnityMvvmToolkit.UITK.BindableUIElements +{ + public partial class BindableDropdownField : DropdownField, IBindableCollection, IInitializable, IDisposable + { + private IProperty _selectedItemProperty; + private IReadOnlyProperty> _itemsSource; + + private PropertyBindingData _selectedItemBindingData; + private PropertyBindingData _itemsSourceBindingData; + + public void Initialize() + { + choices = new List(); + } + + public void Dispose() + { + choices.Clear(); + } + + public void SetBindingContext(IBindingContext context, IObjectProvider objectProvider) + { + if (string.IsNullOrWhiteSpace(BindingItemsSourcePath) == false) + { + _itemsSourceBindingData ??= BindingItemsSourcePath.ToPropertyBindingData(); + _itemsSource = objectProvider + .RentReadOnlyProperty>(context, _itemsSourceBindingData); + _itemsSource.Value.CollectionChanged += OnItemsCollectionChanged; + choices = new List(_itemsSource.Value); + } + + if (string.IsNullOrWhiteSpace(BindingSelectedItemPath) == false) + { + _selectedItemBindingData ??= BindingSelectedItemPath.ToPropertyBindingData(); + _selectedItemProperty = objectProvider.RentProperty(context, _selectedItemBindingData); + _selectedItemProperty.ValueChanged += OnSelectedItemValueChanged; + + var isContains = choices.Contains(_selectedItemProperty.Value); + if (isContains == true) + { + UpdateControlValue(_selectedItemProperty.Value); + } + + this.RegisterValueChangedCallback(OnControlValueChanged); + _selectedItemProperty.Value = choices.Count > 0 ? choices[0] : default; + } + } + + protected virtual void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (string newItem in e.NewItems) + { + choices.Add(newItem); + } + } + + if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (string oldItem in e.OldItems) + { + choices.Remove(oldItem); + } + } + + if (e.Action == NotifyCollectionChangedAction.Reset) + { + choices.Clear(); + } + } + + public virtual void ResetBindingContext(IObjectProvider objectProvider) + { + if (_selectedItemProperty != null) + { + _selectedItemProperty.ValueChanged -= OnSelectedItemValueChanged; + objectProvider.ReturnProperty(_selectedItemProperty); + _selectedItemProperty = null; + this.UnregisterValueChangedCallback(OnControlValueChanged); + } + + if (_itemsSource != null) + { + _itemsSource.Value.CollectionChanged -= OnItemsCollectionChanged; + choices = new List(); + objectProvider.ReturnReadOnlyProperty(_itemsSource); + _itemsSource = null; + } + + UpdateControlValue(default); + } + + protected virtual void OnControlValueChanged(ChangeEvent e) + { + _selectedItemProperty.Value = e.newValue; + } + + private void OnSelectedItemValueChanged(object sender, string newValue) + { + var isContains = choices.Contains(newValue); + if (isContains == false) + { + return; + } + + UpdateControlValue(newValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void UpdateControlValue(string newValue) + { + SetValueWithoutNotify(newValue); + } + } +} \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs.meta new file mode 100644 index 0000000..065981e --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/BindableDropdownField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8c46923c0a364c539ce1f2895dec04ed +timeCreated: 1686411067 \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs new file mode 100644 index 0000000..f89007b --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs @@ -0,0 +1,57 @@ +using UnityEngine.UIElements; +using UnityMvvmToolkit.UITK.Extensions; + +namespace UnityMvvmToolkit.UITK.BindableUIElements +{ + partial class BindableDropdownField + { + public string BindingSelectedItemPath { get; private set; } + + public string BindingItemsSourcePath { get; private set; } + + public new class UxmlFactory : UxmlFactory + { + } + +#if UNITY_2023_2_OR_NEWER + [System.Serializable] + public new class UxmlSerializedData : DropdownField.UxmlSerializedData + { + // ReSharper disable once InconsistentNaming + #pragma warning disable 649 + [UnityEngine.SerializeField] private string BindingSelectedItemPath; + [UnityEngine.SerializeField] private string BindingItemsSourcePath; + #pragma warning restore 649 + + public override object CreateInstance() => new BindableDropdownField(); + public override void Deserialize(object visualElement) + { + base.Deserialize(visualElement); + + var bindableDropdownField = visualElement.As(); + bindableDropdownField.BindingSelectedItemPath = BindingSelectedItemPath; + bindableDropdownField.BindingItemsSourcePath = BindingItemsSourcePath; + } + } +#else + public new class UxmlTraits : DropdownField.UxmlTraits + { + private readonly UxmlStringAttributeDescription _bindingSelectedItemPath = new() + { name = "binding-selected-item-path", defaultValue = "" }; + + private readonly UxmlStringAttributeDescription _bindingItemsSourcePath = new() + { name = "binding-items-source-path", defaultValue = "" }; + + public override void Init(VisualElement visualElement, IUxmlAttributes bag, CreationContext context) + { + base.Init(visualElement, bag, context); + + var bindableDropdownField = visualElement.As(); + + bindableDropdownField.BindingSelectedItemPath = _bindingSelectedItemPath.GetValueFromBag(bag, context); + bindableDropdownField.BindingItemsSourcePath = _bindingItemsSourcePath.GetValueFromBag(bag, context); + } + } +#endif + } +} \ No newline at end of file diff --git a/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs.meta b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs.meta new file mode 100644 index 0000000..32d3949 --- /dev/null +++ b/src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit/Runtime/UITK/BindableUIElements/Uxmls/BindableDropdownField.Uxml.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6c58f7387a96411e88df2f3f4963bb93 +timeCreated: 1686411216 \ No newline at end of file