├── .editorconfig ├── .gitignore ├── Editor.meta ├── Editor ├── AnimatorParameterBindingEditor.cs ├── AnimatorParameterBindingEditor.cs.meta ├── BaseBindingEditor.cs ├── BaseBindingEditor.cs.meta ├── CollectionBindingEditor.cs ├── CollectionBindingEditor.cs.meta ├── EventBindingEditor.cs ├── EventBindingEditor.cs.meta ├── InspectorUtils.cs ├── InspectorUtils.cs.meta ├── OneWayPropertyBindingEditor.cs ├── OneWayPropertyBindingEditor.cs.meta ├── SubViewModelBindingEditor.cs ├── SubViewModelBindingEditor.cs.meta ├── TemplateBindingEditor.cs ├── TemplateBindingEditor.cs.meta ├── TemplateEditor.cs ├── TemplateEditor.cs.meta ├── ToggleActiveBindingEditor.cs ├── ToggleActiveBindingEditor.cs.meta ├── TwoWayPropertyBindingEditor.cs ├── TwoWayPropertyBindingEditor.cs.meta ├── UnityWeld.Editor.asmdef └── UnityWeld.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── AOTOptimisationHelper.cs ├── AOTOptimisationHelper.cs.meta ├── Binding.meta ├── Binding │ ├── AbstractMemberBinding.cs │ ├── AbstractMemberBinding.cs.meta │ ├── AbstractTemplateSelector.cs │ ├── AbstractTemplateSelector.cs.meta │ ├── AdapterOptions.cs │ ├── AdapterOptions.cs.meta │ ├── Adapters.meta │ ├── Adapters │ │ ├── AdapterInfo.cs │ │ ├── AdapterInfo.cs.meta │ │ ├── BaseAdapters.cs │ │ ├── BaseAdapters.cs.meta │ │ ├── BoolToColorAdapterOptions.cs │ │ ├── BoolToColorAdapterOptions.cs.meta │ │ ├── BoolToColorBlockAdapterOptions.cs │ │ ├── BoolToColorBlockAdapterOptions.cs.meta │ │ ├── BoolToStringAdapterOptions.cs │ │ ├── BoolToStringAdapterOptions.cs.meta │ │ ├── ColorToColorBlockAdapterOptions.cs │ │ ├── ColorToColorBlockAdapterOptions.cs.meta │ │ ├── DateTimeToStringAdapterOptions.cs │ │ ├── DateTimeToStringAdapterOptions.cs.meta │ │ ├── FloatToStringAdapterOptions.cs │ │ ├── FloatToStringAdapterOptions.cs.meta │ │ ├── StringCultureToDateTimeAdapterOptions.cs │ │ └── StringCultureToDateTimeAdapterOptions.cs.meta │ ├── AnimatorParameterBinding.cs │ ├── AnimatorParameterBinding.cs.meta │ ├── AnimatorParameterTrigger.cs │ ├── AnimatorParameterTrigger.cs.meta │ ├── BindingAttribute.cs │ ├── BindingAttribute.cs.meta │ ├── BindingHelper.cs │ ├── BindingHelper.cs.meta │ ├── BoundObservableList.cs │ ├── BoundObservableList.cs.meta │ ├── CollectionBinding.cs │ ├── CollectionBinding.cs.meta │ ├── DropdownBinding.cs │ ├── DropdownBinding.cs.meta │ ├── EventBinding.cs │ ├── EventBinding.cs.meta │ ├── Exceptions.meta │ ├── Exceptions │ │ ├── AmbiguousTypeException.cs │ │ ├── AmbiguousTypeException.cs.meta │ │ ├── ComponentNotFoundException.cs │ │ ├── ComponentNotFoundException.cs.meta │ │ ├── InvalidAdapterException.cs │ │ ├── InvalidAdapterException.cs.meta │ │ ├── InvalidEndPointException.cs │ │ ├── InvalidEndPointException.cs.meta │ │ ├── InvalidEventException.cs │ │ ├── InvalidEventException.cs.meta │ │ ├── InvalidTypeException.cs │ │ ├── InvalidTypeException.cs.meta │ │ ├── MemberNotFoundException.cs │ │ ├── MemberNotFoundException.cs.meta │ │ ├── NoSuchAdapterException.cs │ │ ├── NoSuchAdapterException.cs.meta │ │ ├── PropertyNullException.cs │ │ ├── PropertyNullException.cs.meta │ │ ├── TemplateNotFoundException.cs │ │ ├── TemplateNotFoundException.cs.meta │ │ ├── ViewModelNotFoundException.cs │ │ └── ViewModelNotFoundException.cs.meta │ ├── IMemberBinding.cs │ ├── IMemberBinding.cs.meta │ ├── IViewModelProvider.cs │ ├── IViewModelProvider.cs.meta │ ├── Internal.meta │ ├── Internal │ │ ├── BindableMember.cs │ │ ├── BindableMember.cs.meta │ │ ├── PropertyEndPoint.cs │ │ ├── PropertyEndPoint.cs.meta │ │ ├── PropertyFinder.cs │ │ ├── PropertyFinder.cs.meta │ │ ├── PropertySync.cs │ │ ├── PropertySync.cs.meta │ │ ├── PropertyWatcher.cs │ │ ├── PropertyWatcher.cs.meta │ │ ├── TypeResolver.cs │ │ ├── TypeResolver.cs.meta │ │ ├── UnityEventBinder.cs │ │ ├── UnityEventBinder.cs.meta │ │ ├── UnityEventWatcher.cs │ │ └── UnityEventWatcher.cs.meta │ ├── OneWayPropertyBinding.cs │ ├── OneWayPropertyBinding.cs.meta │ ├── SubViewModelBinding.cs │ ├── SubViewModelBinding.cs.meta │ ├── Template.cs │ ├── Template.cs.meta │ ├── TemplateBinding.cs │ ├── TemplateBinding.cs.meta │ ├── ToggleActiveBinding.cs │ ├── ToggleActiveBinding.cs.meta │ ├── TwoWayPropertyBinding.cs │ └── TwoWayPropertyBinding.cs.meta ├── UnityWeld.asmdef ├── UnityWeld.asmdef.meta ├── Widgets.meta └── Widgets │ ├── DropdownAdapter.cs │ └── DropdownAdapter.cs.meta ├── package.json └── package.json.meta /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = crlf 7 | insert_final_newline = false 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimGameDev/Unity-Weld/6d154316ab83d7080e0a9cbdeda487e1b0b857a8/.gitignore -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 72f2efac3eed8eb4f98c8797f42bf210 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/AnimatorParameterBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 922b7495c089af14c907aceb395b0cba 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/BaseBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b2109485ee1df45439d88eef4c5b03a5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/CollectionBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityWeld.Binding; 4 | using UnityWeld.Binding.Internal; 5 | 6 | namespace UnityWeld_Editor 7 | { 8 | [CustomEditor(typeof(CollectionBinding))] 9 | class CollectionBindingEditor : BaseBindingEditor 10 | { 11 | private CollectionBinding _targetScript; 12 | private SerializedProperty _templateInitialPoolCountProperty; 13 | private SerializedProperty _itemsContainerProperty; 14 | private SerializedProperty _templatesProperty; 15 | 16 | private bool _viewModelPrefabModified; 17 | 18 | protected override void OnEnabled() 19 | { 20 | // Initialise everything 21 | _targetScript = (CollectionBinding)target; 22 | _templateInitialPoolCountProperty = serializedObject.FindProperty("_templateInitialPoolCount"); 23 | _itemsContainerProperty = serializedObject.FindProperty("_itemsContainer"); 24 | _templatesProperty = serializedObject.FindProperty("_templates"); 25 | } 26 | 27 | protected override void OnInspector() 28 | { 29 | UpdatePrefabModifiedProperties(); 30 | 31 | EditorGUILayout.PropertyField(_templateInitialPoolCountProperty); 32 | EditorGUILayout.PropertyField(_itemsContainerProperty); 33 | EditorGUILayout.PropertyField(_templatesProperty, true); 34 | 35 | EditorStyles.label.fontStyle = _viewModelPrefabModified ? FontStyle.Bold : DefaultFontStyle; 36 | ShowViewModelPropertyMenu( 37 | new GUIContent("View-model property", "Property on the view-model to bind to."), 38 | TypeResolver.FindBindableCollectionProperties(_targetScript), 39 | updatedValue => _targetScript.ViewModelPropertyName = updatedValue, 40 | _targetScript.ViewModelPropertyName, 41 | property => true 42 | ); 43 | } 44 | 45 | /// 46 | /// Check whether each of the properties on the object have been changed from the value in the prefab. 47 | /// 48 | private void UpdatePrefabModifiedProperties() 49 | { 50 | var property = serializedObject.GetIterator(); 51 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 52 | // will iterate through the properties. 53 | property.Next(true); 54 | do 55 | { 56 | switch (property.name) 57 | { 58 | case "viewModelPropertyName": 59 | _viewModelPrefabModified = property.prefabOverride; 60 | break; 61 | } 62 | } 63 | while (property.Next(false)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Editor/CollectionBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5a455a8f5047aca4fa9c7c67ef994596 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/EventBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityWeld.Binding; 6 | using UnityWeld.Binding.Internal; 7 | 8 | namespace UnityWeld_Editor 9 | { 10 | [CustomEditor(typeof(EventBinding))] 11 | public class EventBindingEditor : BaseBindingEditor 12 | { 13 | private EventBinding targetScript; 14 | 15 | // Whether or not the values on our target match its prefab. 16 | private bool viewEventPrefabModified; 17 | private bool viewModelMethodPrefabModified; 18 | 19 | protected override void OnEnabled() 20 | { 21 | targetScript = (EventBinding)target; 22 | } 23 | 24 | protected override void OnInspector() 25 | { 26 | UpdatePrefabModifiedProperties(); 27 | 28 | EditorStyles.label.fontStyle = viewEventPrefabModified 29 | ? FontStyle.Bold 30 | : DefaultFontStyle; 31 | 32 | ShowEventMenu( 33 | UnityEventWatcher.GetBindableEvents(targetScript.gameObject) 34 | .OrderBy(evt => evt.Name) 35 | .ToArray(), 36 | updatedValue => targetScript.ViewEventName = updatedValue, 37 | targetScript.ViewEventName 38 | ); 39 | 40 | EditorStyles.label.fontStyle = viewModelMethodPrefabModified 41 | ? FontStyle.Bold 42 | : DefaultFontStyle; 43 | 44 | ShowMethodMenu(targetScript, TypeResolver.FindBindableMethods(targetScript)); 45 | } 46 | 47 | /// 48 | /// Draws the dropdown for selecting a method from bindableViewModelMethods 49 | /// 50 | private void ShowMethodMenu( 51 | EventBinding targetScript, 52 | BindableMember[] bindableMethods 53 | ) 54 | { 55 | var tooltip = "Method on the view-model to bind to."; 56 | 57 | InspectorUtils.DoPopup( 58 | new GUIContent(targetScript.ViewModelMethodName), 59 | new GUIContent("View-model method", tooltip), 60 | m => m.ViewModelType + "/" + m.MemberName, 61 | m => true, 62 | m => m.ToString() == targetScript.ViewModelMethodName, 63 | m => UpdateProperty( 64 | updatedValue => targetScript.ViewModelMethodName = updatedValue, 65 | targetScript.ViewModelMethodName, 66 | m.ToString(), 67 | "Set bound view-model method" 68 | ), 69 | bindableMethods 70 | .OrderBy(m => m.ViewModelTypeName) 71 | .ThenBy(m => m.MemberName) 72 | .ToArray() 73 | ); 74 | } 75 | 76 | /// 77 | /// Check whether each of the properties on the object have been changed 78 | /// from the value in the prefab. 79 | /// 80 | private void UpdatePrefabModifiedProperties() 81 | { 82 | var property = serializedObject.GetIterator(); 83 | // Need to call Next(true) to get the first child. Once we have it, 84 | // Next(false) will iterate through the properties. 85 | property.Next(true); 86 | do 87 | { 88 | switch (property.name) 89 | { 90 | case "viewEventName": 91 | viewEventPrefabModified = property.prefabOverride; 92 | break; 93 | 94 | case "viewModelMethodName": 95 | viewModelMethodPrefabModified = property.prefabOverride; 96 | break; 97 | } 98 | } 99 | while (property.Next(false)); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Editor/EventBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 375353919d68d544fadc2369d1a012d5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/InspectorUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEditor.SceneManagement; 4 | using UnityEngine; 5 | 6 | namespace UnityWeld_Editor 7 | { 8 | /// 9 | /// Common utilities for custom inspectors. 10 | /// 11 | internal class InspectorUtils 12 | { 13 | /// 14 | /// Show a popup menu with some items disabled and a label to its left. 15 | /// 16 | public static void DoPopup( 17 | GUIContent content, 18 | GUIContent label, 19 | Func menuName, 20 | Func menuEnabled, 21 | Func isSelected, 22 | Action callback, 23 | T[] items) 24 | { 25 | var labelRect = EditorGUILayout.GetControlRect(false, 16f, EditorStyles.popup); 26 | var controlId = GUIUtility.GetControlID(FocusType.Keyboard, labelRect); 27 | 28 | var buttonRect = EditorGUI.PrefixLabel(labelRect, controlId, label); 29 | 30 | ShowPopupButton( 31 | buttonRect, 32 | labelRect, 33 | controlId, 34 | content, 35 | () => ShowMenu(menuName, menuEnabled, isSelected, callback, items, buttonRect) 36 | ); 37 | } 38 | 39 | /// 40 | /// Shows the button for a popup/dropdown control, with a label. 41 | /// 42 | private static void ShowPopupButton(Rect buttonRect, Rect labelRect, int controlId, GUIContent currentlySelected, Action popup) 43 | { 44 | var currentEvent = Event.current; 45 | var eventType = currentEvent.type; 46 | var style = EditorStyles.popup; 47 | 48 | switch (eventType) 49 | { 50 | case EventType.KeyDown: 51 | if (MainActionKeyForControl(currentEvent, controlId)) 52 | { 53 | popup(); 54 | currentEvent.Use(); 55 | } 56 | break; 57 | 58 | case EventType.Repaint: 59 | style.Draw(buttonRect, currentlySelected, controlId, false); 60 | break; 61 | 62 | case EventType.MouseDown: 63 | if (currentEvent.button != 0) 64 | { 65 | return; 66 | } 67 | 68 | if (buttonRect.Contains(currentEvent.mousePosition)) 69 | { 70 | popup(); 71 | GUIUtility.keyboardControl = controlId; 72 | currentEvent.Use(); 73 | } 74 | else if (labelRect.Contains(currentEvent.mousePosition)) 75 | { 76 | GUIUtility.keyboardControl = controlId; 77 | currentEvent.Use(); 78 | } 79 | break; 80 | } 81 | } 82 | 83 | /// 84 | /// Returns whether the specified control has been activated by a key press. 85 | /// 86 | private static bool MainActionKeyForControl(Event evt, int controlId) 87 | { 88 | if (GUIUtility.keyboardControl != controlId) 89 | { 90 | return false; 91 | } 92 | bool modifierPressed = evt.alt || evt.shift || evt.command || evt.control; 93 | if (!modifierPressed && evt.type == EventType.KeyDown && evt.character == ' ') 94 | { 95 | evt.Use(); 96 | return false; 97 | } 98 | return evt.type == EventType.KeyDown 99 | && (evt.keyCode == KeyCode.Space || evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) 100 | && !modifierPressed; 101 | } 102 | 103 | /// 104 | /// Show a menu with some items disabled. Has a callback that will be called when an item is selected with the index of the selected item. 105 | /// Takes a dictionary of options and whether or not they should be enabled. 106 | /// 107 | private static void ShowMenu(Func menuName, Func menuEnabled, Func isSelected, Action callback, T[] items, Rect position) 108 | { 109 | var menu = new GenericMenu(); 110 | 111 | for (var i = 0; i < items.Length; i++) 112 | { 113 | // Need to cache index so that it doesn't get passed through to the callback by reference. 114 | int index = i; 115 | var item = items[index]; 116 | 117 | var content = new GUIContent(menuName(item)); 118 | 119 | if (menuEnabled(item)) 120 | { 121 | menu.AddItem(content, isSelected(item), () => callback(item)); 122 | } 123 | else 124 | { 125 | menu.AddDisabledItem(content); 126 | } 127 | } 128 | 129 | menu.DropDown(position); 130 | } 131 | 132 | /// 133 | /// Tell Unity that a change has been made to a specified object and we have to save the scene. 134 | /// 135 | public static void MarkSceneDirty(GameObject gameObject) 136 | { 137 | // TODO: Undo.RecordObject also marks the scene dirty, so this will no longer be necessary once undo support is added. 138 | EditorSceneManager.MarkSceneDirty(gameObject.scene); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Editor/InspectorUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d7b4b21f4dc5f004689c8d4d822d8a09 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/OneWayPropertyBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEditor.AnimatedValues; 5 | using UnityEngine; 6 | using UnityWeld.Binding; 7 | using UnityWeld.Binding.Internal; 8 | 9 | namespace UnityWeld_Editor 10 | { 11 | [CustomEditor(typeof(OneWayPropertyBinding))] 12 | class OneWayPropertyBindingEditor : BaseBindingEditor 13 | { 14 | private OneWayPropertyBinding targetScript; 15 | 16 | private AnimBool viewAdapterOptionsFade; 17 | 18 | // Whether each property in the target differs from the prefab it uses. 19 | private bool viewAdapterPrefabModified; 20 | private bool viewAdapterOptionsPrefabModified; 21 | private bool viewModelPropertyPrefabModified; 22 | private bool viewPropertyPrefabModified; 23 | 24 | protected override void OnEnabled() 25 | { 26 | // Initialise reference to target script 27 | targetScript = (OneWayPropertyBinding)target; 28 | 29 | viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); 30 | viewAdapterOptionsFade.valueChanged.AddListener(Repaint); 31 | } 32 | 33 | private void OnDisable() 34 | { 35 | viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); 36 | } 37 | 38 | protected override void OnInspector() 39 | { 40 | UpdatePrefabModifiedProperties(); 41 | 42 | var defaultLabelStyle = EditorStyles.label.fontStyle; 43 | EditorStyles.label.fontStyle = viewPropertyPrefabModified 44 | ? FontStyle.Bold 45 | : defaultLabelStyle; 46 | 47 | Type viewPropertyType; 48 | ShowViewPropertyMenu( 49 | new GUIContent("View property", "Property on the view to bind to"), 50 | PropertyFinder.GetBindableProperties(targetScript.gameObject), 51 | updatedValue => targetScript.ViewPropertyName = updatedValue, 52 | targetScript.ViewPropertyName, 53 | out viewPropertyType 54 | ); 55 | 56 | // Don't let the user set anything else until they've chosen a view property. 57 | var guiPreviouslyEnabled = GUI.enabled; 58 | if (string.IsNullOrEmpty(targetScript.ViewPropertyName)) 59 | { 60 | GUI.enabled = false; 61 | } 62 | 63 | var viewAdapterTypeNames = TypeResolver.GetAdapterIds( 64 | o => viewPropertyType == null || o.OutType == viewPropertyType); 65 | 66 | EditorStyles.label.fontStyle = viewAdapterPrefabModified 67 | ? FontStyle.Bold 68 | : defaultLabelStyle; 69 | 70 | ShowAdapterMenu( 71 | new GUIContent( 72 | "View adapter", 73 | "Adapter that converts values sent from the view-model to the view." 74 | ), 75 | viewAdapterTypeNames, 76 | targetScript.ViewAdapterId, 77 | newValue => 78 | { 79 | // Get rid of old adapter options if we changed the type of the adapter. 80 | if (newValue != targetScript.ViewAdapterId) 81 | { 82 | Undo.RecordObject(targetScript, "Set view adapter options"); 83 | targetScript.ViewAdapterOptions = null; 84 | } 85 | 86 | UpdateProperty( 87 | updatedValue => targetScript.ViewAdapterId = updatedValue, 88 | targetScript.ViewAdapterId, 89 | newValue, 90 | "Set view adapter" 91 | ); 92 | } 93 | ); 94 | 95 | Type adapterType; 96 | viewAdapterOptionsFade.target = ShouldShowAdapterOptions( 97 | targetScript.ViewAdapterId, 98 | out adapterType 99 | ); 100 | 101 | EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified 102 | ? FontStyle.Bold 103 | : defaultLabelStyle; 104 | 105 | ShowAdapterOptionsMenu( 106 | "View adapter options", 107 | adapterType, 108 | options => targetScript.ViewAdapterOptions = options, 109 | targetScript.ViewAdapterOptions, 110 | viewAdapterOptionsFade.faded 111 | ); 112 | 113 | EditorGUILayout.Space(); 114 | 115 | EditorStyles.label.fontStyle = viewModelPropertyPrefabModified 116 | ? FontStyle.Bold 117 | : defaultLabelStyle; 118 | 119 | var adaptedViewPropertyType = AdaptTypeBackward( 120 | viewPropertyType, 121 | targetScript.ViewAdapterId 122 | ); 123 | ShowViewModelPropertyMenu( 124 | new GUIContent( 125 | "View-model property", 126 | "Property on the view-model to bind to." 127 | ), 128 | TypeResolver.FindBindableProperties(targetScript), 129 | updatedValue => targetScript.ViewModelPropertyName = updatedValue, 130 | targetScript.ViewModelPropertyName, 131 | property => property.PropertyType == adaptedViewPropertyType 132 | ); 133 | 134 | GUI.enabled = guiPreviouslyEnabled; 135 | 136 | EditorStyles.label.fontStyle = defaultLabelStyle; 137 | } 138 | 139 | /// 140 | /// Check whether each of the properties on the object have been changed 141 | /// from the value in the prefab. 142 | /// 143 | private void UpdatePrefabModifiedProperties() 144 | { 145 | var property = serializedObject.GetIterator(); 146 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 147 | // will iterate through the properties. 148 | property.Next(true); 149 | do 150 | { 151 | switch (property.name) 152 | { 153 | case "viewAdapterTypeName": 154 | viewAdapterPrefabModified = property.prefabOverride; 155 | break; 156 | 157 | case "viewAdapterOptions": 158 | viewAdapterOptionsPrefabModified = property.prefabOverride; 159 | break; 160 | 161 | case "viewModelPropertyName": 162 | viewModelPropertyPrefabModified = property.prefabOverride; 163 | break; 164 | 165 | case "viewPropertyName": 166 | viewPropertyPrefabModified = property.prefabOverride; 167 | break; 168 | } 169 | } 170 | while (property.Next(false)); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Editor/OneWayPropertyBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 362976f353b49a545856b39257e9eb02 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/SubViewModelBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityWeld.Binding; 4 | using UnityWeld.Binding.Internal; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace UnityWeld_Editor 9 | { 10 | /// 11 | /// Inspector window for SubViewModelBinding 12 | /// 13 | [CustomEditor(typeof(SubViewModelBinding))] 14 | public class SubViewModelBindingEditor : BaseBindingEditor 15 | { 16 | private SubViewModelBinding targetScript; 17 | 18 | /// 19 | /// Whether or not the value on our target matches its prefab. 20 | /// 21 | private bool propertyPrefabModified; 22 | 23 | protected override void OnEnabled() 24 | { 25 | targetScript = (SubViewModelBinding)target; 26 | } 27 | 28 | protected override void OnInspector() 29 | { 30 | UpdatePrefabModifiedProperties(); 31 | 32 | var bindableProperties = FindBindableProperties(); 33 | 34 | EditorStyles.label.fontStyle = propertyPrefabModified 35 | ? FontStyle.Bold 36 | : DefaultFontStyle; 37 | 38 | ShowViewModelPropertyMenu( 39 | new GUIContent( 40 | "Sub view-model property", 41 | "The property on the top level view model containing the sub view-model" 42 | ), 43 | bindableProperties, 44 | updatedValue => 45 | { 46 | targetScript.ViewModelPropertyName = updatedValue; 47 | 48 | targetScript.ViewModelTypeName = bindableProperties 49 | .Single(prop => prop.ToString() == updatedValue) 50 | .Member.PropertyType.ToString(); 51 | }, 52 | targetScript.ViewModelPropertyName, 53 | p => true 54 | ); 55 | } 56 | 57 | private BindableMember[] FindBindableProperties() 58 | { 59 | return TypeResolver.FindBindableProperties(targetScript) 60 | .Where(prop => prop.Member.PropertyType.HasBindingAttribute() 61 | ) 62 | .ToArray(); 63 | } 64 | 65 | /// 66 | /// Check whether each of the properties on the object have been changed 67 | /// from the value in the prefab. 68 | /// 69 | private void UpdatePrefabModifiedProperties() 70 | { 71 | var property = serializedObject.GetIterator(); 72 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 73 | // will iterate through the properties. 74 | 75 | propertyPrefabModified = false; 76 | property.Next(true); 77 | do 78 | { 79 | switch (property.name) 80 | { 81 | case "viewModelPropertyName": 82 | case "viewModelTypeName": 83 | propertyPrefabModified = property.prefabOverride 84 | || propertyPrefabModified; 85 | break; 86 | } 87 | } 88 | while (property.Next(false)); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Editor/SubViewModelBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a40c8489f721eb24991cefbc29d4e6fa 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/TemplateBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityWeld.Binding; 4 | using UnityWeld.Binding.Internal; 5 | 6 | namespace UnityWeld_Editor 7 | { 8 | [CustomEditor(typeof(TemplateBinding))] 9 | class TemplateBindingEditor : BaseBindingEditor 10 | { 11 | private TemplateBinding targetScript; 12 | 13 | private bool viewModelPrefabModified; 14 | private SerializedProperty _templatesProperty; 15 | 16 | protected override void OnEnabled() 17 | { 18 | targetScript = (TemplateBinding)target; 19 | _templatesProperty = serializedObject.FindProperty("_templates"); 20 | } 21 | 22 | protected override void OnInspector() 23 | { 24 | UpdatePrefabModifiedProperties(); 25 | 26 | EditorStyles.label.fontStyle = viewModelPrefabModified 27 | ? FontStyle.Bold 28 | : DefaultFontStyle; 29 | 30 | ShowViewModelPropertyMenu( 31 | new GUIContent( 32 | "Template property", 33 | "Property on the view model to use for selecting templates." 34 | ), 35 | TypeResolver.FindBindableProperties(targetScript), 36 | updatedValue => targetScript.ViewModelPropertyName = updatedValue, 37 | targetScript.ViewModelPropertyName, 38 | property => true 39 | ); 40 | 41 | EditorGUILayout.PropertyField(_templatesProperty, true); 42 | } 43 | 44 | /// 45 | /// Check whether each of the properties on the object have been changed 46 | /// from the value in the prefab. 47 | /// 48 | private void UpdatePrefabModifiedProperties() 49 | { 50 | var property = serializedObject.GetIterator(); 51 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 52 | // will iterate through the properties. 53 | property.Next(true); 54 | do 55 | { 56 | switch (property.name) 57 | { 58 | case "viewModelPropertyName": 59 | viewModelPrefabModified = property.prefabOverride; 60 | break; 61 | } 62 | } 63 | while (property.Next(false)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Editor/TemplateBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b4ca5578372f3924fb0e72cdea97397d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/TemplateEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityWeld.Binding; 6 | using UnityWeld.Binding.Internal; 7 | 8 | namespace UnityWeld_Editor 9 | { 10 | /// 11 | /// Editor for template bindings with a dropdown for selecting what view model 12 | /// to bind to. 13 | /// 14 | [CustomEditor(typeof(Template))] 15 | public class TemplateEditor : BaseBindingEditor 16 | { 17 | private Template targetScript; 18 | 19 | /// 20 | /// Whether the value on our target matches its prefab. 21 | /// 22 | private bool propertyPrefabModified; 23 | 24 | protected override void OnEnabled() 25 | { 26 | targetScript = (Template)target; 27 | } 28 | 29 | protected override void OnInspector() 30 | { 31 | UpdatePrefabModifiedProperties(); 32 | 33 | var availableViewModels = TypeResolver.TypesWithBindingAttribute 34 | .Select(type => type.ToString()) 35 | .OrderBy(name => name) 36 | .ToArray(); 37 | 38 | var selectedIndex = Array.IndexOf( 39 | availableViewModels, 40 | targetScript.ViewModelTypeName 41 | ); 42 | 43 | EditorStyles.label.fontStyle = propertyPrefabModified 44 | ? FontStyle.Bold 45 | : DefaultFontStyle; 46 | 47 | var newSelectedIndex = EditorGUILayout.Popup( 48 | new GUIContent( 49 | "Template view model", 50 | "Type of the view model that this template will be bound to when it is instantiated." 51 | ), 52 | selectedIndex, 53 | availableViewModels 54 | .Select(viewModel => new GUIContent(viewModel)) 55 | .ToArray() 56 | ); 57 | 58 | EditorStyles.label.fontStyle = DefaultFontStyle; 59 | 60 | UpdateProperty(newValue => targetScript.ViewModelTypeName = newValue, 61 | selectedIndex < 0 62 | ? string.Empty 63 | : availableViewModels[selectedIndex], 64 | newSelectedIndex < 0 65 | ? string.Empty 66 | : availableViewModels[newSelectedIndex], 67 | "Set bound view-model for template" 68 | ); 69 | } 70 | 71 | /// 72 | /// Check whether each of the properties on the object have been changed from the value in the prefab. 73 | /// 74 | private void UpdatePrefabModifiedProperties() 75 | { 76 | var property = serializedObject.GetIterator(); 77 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 78 | // will iterate through the properties. 79 | property.Next(true); 80 | do 81 | { 82 | if (property.name == "viewModelTypeName") 83 | { 84 | propertyPrefabModified = property.prefabOverride; 85 | } 86 | } 87 | while (property.Next(false)); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Editor/TemplateEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 41e9680918dbe374abf0efb0d8d9884d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ToggleActiveBindingEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEditor.AnimatedValues; 4 | using UnityEngine; 5 | using UnityWeld.Binding; 6 | using UnityWeld.Binding.Internal; 7 | 8 | namespace UnityWeld_Editor 9 | { 10 | [CustomEditor(typeof(ToggleActiveBinding))] 11 | public class ToggleActiveBindingEditor : BaseBindingEditor 12 | { 13 | private ToggleActiveBinding targetScript; 14 | 15 | private AnimBool viewAdapterOptionsFade; 16 | 17 | private bool viewAdapterPrefabModified; 18 | private bool viewAdapterOptionsPrefabModified; 19 | private bool viewModelPropertyPrefabModified; 20 | 21 | protected override void OnEnabled() 22 | { 23 | targetScript = (ToggleActiveBinding)target; 24 | 25 | viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); 26 | viewAdapterOptionsFade.valueChanged.AddListener(Repaint); 27 | } 28 | 29 | private void OnDisable() 30 | { 31 | viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); 32 | } 33 | 34 | protected override void OnInspector() 35 | { 36 | UpdatePrefabModifiedProperties(); 37 | 38 | var viewPropertyType = typeof(bool); 39 | 40 | var viewAdapterTypeNames = TypeResolver.GetAdapterIds(o => o.OutType == viewPropertyType); 41 | 42 | EditorStyles.label.fontStyle = viewAdapterPrefabModified 43 | ? FontStyle.Bold 44 | : DefaultFontStyle; 45 | 46 | ShowAdapterMenu( 47 | new GUIContent( 48 | "View adapter", 49 | "Adapter that converts values sent from the view-model to the view." 50 | ), 51 | viewAdapterTypeNames, 52 | targetScript.ViewAdapterId, 53 | newValue => 54 | { 55 | // Get rid of old adapter options if we changed the type of the adapter. 56 | if (newValue != targetScript.ViewAdapterId) 57 | { 58 | Undo.RecordObject(targetScript, "Set view adapter options"); 59 | targetScript.ViewAdapterOptions = null; 60 | } 61 | 62 | UpdateProperty( 63 | updatedValue => targetScript.ViewAdapterId = updatedValue, 64 | targetScript.ViewAdapterId, 65 | newValue, 66 | "Set view adapter" 67 | ); 68 | } 69 | ); 70 | 71 | Type adapterType; 72 | viewAdapterOptionsFade.target = ShouldShowAdapterOptions( 73 | targetScript.ViewAdapterId, 74 | out adapterType 75 | ); 76 | 77 | EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified 78 | ? FontStyle.Bold 79 | : DefaultFontStyle; 80 | 81 | ShowAdapterOptionsMenu( 82 | "View adapter options", 83 | adapterType, 84 | options => targetScript.ViewAdapterOptions = options, 85 | targetScript.ViewAdapterOptions, 86 | viewAdapterOptionsFade.faded 87 | ); 88 | 89 | EditorGUILayout.Space(); 90 | 91 | EditorStyles.label.fontStyle = viewModelPropertyPrefabModified 92 | ? FontStyle.Bold 93 | : DefaultFontStyle; 94 | 95 | var adaptedViewPropertyType = AdaptTypeBackward( 96 | viewPropertyType, 97 | targetScript.ViewAdapterId 98 | ); 99 | ShowViewModelPropertyMenu( 100 | new GUIContent( 101 | "View-model property", 102 | "Property on the view-model to bind to." 103 | ), 104 | TypeResolver.FindBindableProperties(targetScript), 105 | updatedValue => targetScript.ViewModelPropertyName = updatedValue, 106 | targetScript.ViewModelPropertyName, 107 | property => property.PropertyType == adaptedViewPropertyType 108 | ); 109 | } 110 | 111 | private void UpdatePrefabModifiedProperties() 112 | { 113 | var property = serializedObject.GetIterator(); 114 | // Need to call Next(true) to get the first child. Once we have it, Next(false) 115 | // will iterate through the properties. 116 | property.Next(true); 117 | do 118 | { 119 | switch (property.name) 120 | { 121 | case "viewAdapterTypeName": 122 | viewAdapterPrefabModified = property.prefabOverride; 123 | break; 124 | 125 | case "viewAdapterOptions": 126 | viewAdapterOptionsPrefabModified = property.prefabOverride; 127 | break; 128 | 129 | case "viewModelPropertyName": 130 | viewModelPropertyPrefabModified = property.prefabOverride; 131 | break; 132 | } 133 | } 134 | while (property.Next(false)); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Editor/ToggleActiveBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e2ae3e18ed11fbd46af8d4ae70099c0e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/TwoWayPropertyBindingEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2c7ae872ed2100b46a600b9b54780bf0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/UnityWeld.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityWeld.Editor", 3 | "rootNamespace": "UnityWeld_Editor", 4 | "references": [ 5 | "GUID:af0045edb0742c34c9f8aefb8323e52c" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /Editor/UnityWeld.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c29ab574f158e5945b4ec0492d3c47e7 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Real Serious Games 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 950ecacf92f7f7743b5e5d373d964b7f 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity-Weld 2 | *[MVVM-style](https://msdn.microsoft.com/en-us/library/hh848246.aspx) data-binding system for Unity.* 3 | 4 | Unity-Weld is a library for Unity 2019+ that enables two-way data binding between Unity UI widgets and game/business logic code. This reduces boiler-plate code that would otherwise be necessary for things like updating the UI when a property changes, removes the need for messy links between objects in the scene that can be broken easily, and allows easier unit testing of code by providing a layer of abstraction between the UI and your core logic code. 5 | 6 | A series of articles on Unity Weld has been published on [What Could Possibly Go Wrong](http://www.what-could-possibly-go-wrong.com/bringing-mvvm-to-unity-part-1-about-mvvm-and-unity-weld). 7 | 8 | FOR ORIGINAL FORK: Example Unity project can be found here: [https://github.com/Real-Serious-Games/Unity-Weld-Examples](https://github.com/Real-Serious-Games/Unity-Weld-Examples). 9 | 10 | ## Installation 11 | 12 | To install Unity-Weld in a new or existing Unity project: 13 | Option 1: use package.json to locally install as UPM package using UPM package mananger 14 | Option 2: generate upm package using npm tool (npm pack), upload to your feed and add your feed to Unity Package Manager 15 | 16 | Alternatively, just copy the `Editor` to `Scripts/UnityWeld/Editor` and `Runtime` to `Scripts/UnityWeld/Runtime` into your `Assets` directory in your Unity project. 17 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 43a15502f0f199743b3e22834c7408ee 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ece62dfb5dd94349a31b908b4e1355d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/AOTOptimisationHelper.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.EventSystems; 3 | using UnityWeld.Binding.Internal; 4 | // ReSharper disable UnusedMember.Global 5 | // ReSharper disable UnusedMember.Local 6 | // ReSharper disable UnusedVariable 7 | 8 | #pragma warning disable 219 // Disable warning that variable is never used 9 | 10 | namespace UnityWeld 11 | { 12 | /// 13 | /// In order for certain generic types to not be optimised-out by IL2CPP for 14 | /// platforms like Xbox One, iPhone and WebGL, we need to reference them at 15 | /// least once in code instead of just calling them via reflection. 16 | /// 17 | /// See this page for more details: 18 | /// https://docs.unity3d.com/Manual/TroubleShootingIPhone.html 19 | /// In the section "The game crashes with the error message “ExecutionEngineException: 20 | /// Attempting to JIT compile method ‘SometType`1<SomeValueType>:.ctor ()’ while 21 | /// running with –aot-only.”" 22 | /// 23 | internal class AOTOptimisationHelper 24 | { 25 | // Even though this method is never called, the fact that it exists will 26 | // ensure the compiler includes the types referenced in it so that we can 27 | // later refer to those via reflection. 28 | private void EnsureGenericTypes() 29 | { 30 | // Used by InputField 31 | var strEventBinder = new UnityEventBinder(null, null); 32 | 33 | // Used by Slider and Scrollbar 34 | var floatEventBinder = new UnityEventBinder(null, null); 35 | 36 | // Used by Toggle 37 | var boolEventBinder = new UnityEventBinder(null, null); 38 | 39 | // Used by Dropdown 40 | var intEventBinder = new UnityEventBinder(null, null); 41 | 42 | // Used by ScrollRect 43 | var vector2EventBinder = new UnityEventBinder(null, null); 44 | 45 | // Used by ColorTween 46 | var colorEventBinder = new UnityEventBinder(null, null); 47 | 48 | // Used by EventTrigger 49 | var baseEventDataEventBinder = new UnityEventBinder(null, null); 50 | } 51 | } 52 | } 53 | 54 | #pragma warning restore 219 -------------------------------------------------------------------------------- /Runtime/AOTOptimisationHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b067127a7e01f0408d7cb11797df5c0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1f7dff4136c1c10459fc630a239805ee 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Binding/AbstractMemberBinding.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using UnityEngine; 3 | using UnityWeld.Binding.Exceptions; 4 | using UnityWeld.Binding.Internal; 5 | 6 | namespace UnityWeld.Binding 7 | { 8 | /// 9 | /// Base class for binders to Unity MonoBehaviours. 10 | /// 11 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 12 | public abstract class AbstractMemberBinding : MonoBehaviour, IMemberBinding 13 | { 14 | private bool _isInitCalled; 15 | 16 | [SerializeField, Header("Automatically bind once on \"OnEnable()\"")] 17 | private bool _isAutoConnection; 18 | 19 | 20 | /// 21 | /// Initialise this binding. Used when we first start the scene. 22 | /// Detaches any attached view models, finds available view models afresh and then connects the binding. 23 | /// 24 | public virtual void Init() 25 | { 26 | if(_isAutoConnection && !gameObject.activeInHierarchy) 27 | { 28 | return; //wait for enabling 29 | } 30 | 31 | if (_isInitCalled) 32 | { 33 | return; //avoid double connect 34 | } 35 | 36 | _isInitCalled = true; 37 | 38 | Disconnect(); 39 | Connect(); 40 | } 41 | 42 | /// 43 | /// Scan up the hierarchy and find a view model that corresponds to the specified name. 44 | /// 45 | private object FindViewModel(string viewModelName) 46 | { 47 | var trans = transform; 48 | while(trans != null) 49 | { 50 | using(var cache = trans.gameObject.GetComponentsWithCache(false)) 51 | { 52 | var monoBehaviourViewModel = cache.Components 53 | .FirstOrDefault(component => component.GetType().ToString() == viewModelName); 54 | if(monoBehaviourViewModel != null) 55 | { 56 | return monoBehaviourViewModel; 57 | } 58 | 59 | var providedViewModel = cache.Components 60 | .Select(component => component.GetViewModelData()) 61 | .Where(component => component != null) 62 | .FirstOrDefault(viewModelData => viewModelData.TypeName == viewModelName); 63 | 64 | if(providedViewModel != null) 65 | { 66 | return providedViewModel.Model; 67 | } 68 | } 69 | 70 | trans = trans.parent; 71 | } 72 | 73 | throw new ViewModelNotFoundException( 74 | $"Tried to get view model {viewModelName} but it could not be found on " + 75 | $"object {gameObject.name}. Check that a ViewModelBinding for that view model exists further up in " + 76 | "the scene hierarchy. " 77 | ); 78 | } 79 | 80 | /// 81 | /// Make a property end point for a property on the view model. 82 | /// 83 | protected PropertyEndPoint MakeViewModelEndPoint(string viewModelPropertyName, string adapterId, 84 | AdapterOptions adapterOptions) 85 | { 86 | string propertyName; 87 | object viewModel; 88 | ParseViewModelEndPointReference(viewModelPropertyName, out propertyName, out viewModel); 89 | 90 | var adapter = TypeResolver.GetAdapter(adapterId); 91 | return new PropertyEndPoint(viewModel, propertyName, adapter, adapterOptions, "view-model", this); 92 | } 93 | 94 | /// 95 | /// Parse an end-point reference including a type name and member name separated by a period. 96 | /// 97 | protected static void ParseEndPointReference(string endPointReference, out string memberName, 98 | out string typeName) 99 | { 100 | var lastPeriodIndex = endPointReference.LastIndexOf('.'); 101 | if(lastPeriodIndex == -1) 102 | { 103 | throw new InvalidEndPointException( 104 | "No period was found, expected end-point reference in the following format: .. " + 105 | "Provided end-point reference: " + endPointReference 106 | ); 107 | } 108 | 109 | typeName = endPointReference.Substring(0, lastPeriodIndex); 110 | memberName = endPointReference.Substring(lastPeriodIndex + 1); 111 | //Due to (undocumented) unity behaviour, some of their components do not work with the namespace when using GetComponent(""), and all of them work without the namespace 112 | //So to be safe, we remove all namespaces from any component that starts with UnityEngine 113 | if(typeName.StartsWith("UnityEngine.")) 114 | { 115 | typeName = typeName.Substring(typeName.LastIndexOf('.') + 1); 116 | } 117 | 118 | if(typeName.Length == 0 || memberName.Length == 0) 119 | { 120 | throw new InvalidEndPointException( 121 | "Bad format for end-point reference, expected the following format: .. " + 122 | "Provided end-point reference: " + endPointReference 123 | ); 124 | } 125 | } 126 | 127 | /// 128 | /// Parse an end-point reference and search up the hierarchy for the named view-model. 129 | /// 130 | protected void ParseViewModelEndPointReference(string endPointReference, out string memberName, 131 | out object viewModel) 132 | { 133 | string viewModelName; 134 | ParseEndPointReference(endPointReference, out memberName, out viewModelName); 135 | 136 | viewModel = FindViewModel(viewModelName); 137 | if(viewModel == null) 138 | { 139 | throw new ViewModelNotFoundException("Failed to find view-model in hierarchy: " + viewModelName); 140 | } 141 | } 142 | 143 | /// 144 | /// Parse an end-point reference and get the component for the view. 145 | /// 146 | protected void ParseViewEndPointReference(string endPointReference, out string memberName, out Component view) 147 | { 148 | string boundComponentType; 149 | ParseEndPointReference(endPointReference, out memberName, out boundComponentType); 150 | 151 | view = GetComponent(boundComponentType); 152 | if(view == null) 153 | { 154 | throw new ComponentNotFoundException("Failed to find component on current game object: " + 155 | boundComponentType); 156 | } 157 | } 158 | 159 | /// 160 | /// Connect to all the attached view models 161 | /// 162 | public abstract void Connect(); 163 | 164 | /// 165 | /// Disconnect from all attached view models. 166 | /// 167 | public abstract void Disconnect(); 168 | 169 | /// 170 | /// Standard MonoBehaviour awake message, do not call this explicitly. 171 | /// Initialises the binding. 172 | /// 173 | protected void OnEnable() 174 | { 175 | if (!_isAutoConnection || _isInitCalled) 176 | { 177 | return; 178 | } 179 | 180 | Init(); 181 | } 182 | 183 | /// 184 | /// Clean up when the game object is destroyed. 185 | /// 186 | public virtual void OnDestroy() 187 | { 188 | Disconnect(); 189 | } 190 | 191 | public void ResetBinding() 192 | { 193 | _isInitCalled = false; 194 | Disconnect(); 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /Runtime/Binding/AbstractMemberBinding.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cbecddc181c8f044dadab0f55a663934 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/AbstractTemplateSelector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | using UnityEngine.Assertions; 6 | using UnityWeld.Binding.Exceptions; 7 | using UnityWeld.Binding.Internal; 8 | 9 | namespace UnityWeld.Binding 10 | { 11 | public abstract class AbstractTemplateSelector : AbstractMemberBinding 12 | { 13 | [Header("Set templates for collection")] 14 | [SerializeField] private Template[] _templates; 15 | [SerializeField] private string viewModelPropertyName = string.Empty; 16 | 17 | private IDictionary _availableTemplates; 18 | 19 | /// 20 | /// All the child objects that have been created, indexed by the view they are connected to. 21 | /// 22 | private readonly IDictionary _instantiatedTemplates = new Dictionary(); 23 | 24 | 25 | /// 26 | /// The view-model, cached during connection. 27 | /// 28 | protected object ViewModel; 29 | 30 | /// 31 | /// Watches the view-model property for changes. 32 | /// 33 | protected PropertyWatcher ViewModelPropertyWatcher; 34 | 35 | /// 36 | /// The name of the property we are binding to on the view model. 37 | /// 38 | public string ViewModelPropertyName 39 | { 40 | get => viewModelPropertyName; 41 | set => viewModelPropertyName = value; 42 | } 43 | 44 | public Template[] Templates 45 | { 46 | get => _templates; 47 | set => _templates = value; 48 | } 49 | 50 | /// 51 | /// All available templates indexed by the view model the are for. 52 | /// 53 | protected IDictionary AvailableTemplates 54 | { 55 | get 56 | { 57 | if(_availableTemplates == null) 58 | { 59 | CacheTemplates(); 60 | } 61 | 62 | return _availableTemplates; 63 | } 64 | } 65 | 66 | // Cache available templates. 67 | private void CacheTemplates() 68 | { 69 | if(_templates == null || _templates.Length == 0) 70 | { 71 | return; 72 | } 73 | 74 | _availableTemplates = new Dictionary(); 75 | 76 | 77 | _availableTemplates = _templates 78 | .Select(template => 79 | { 80 | var typeName = template.GetViewModelTypeName(); 81 | var type = TypeResolver.TypesWithBindingAttribute 82 | .FirstOrDefault(t => string.Equals(t.ToString(), typeName, StringComparison.Ordinal)); 83 | 84 | if (type == null) 85 | { 86 | throw new Exception($"Unable to find type with binding attribute: {typeName}. Template name: {template.name}"); 87 | } 88 | 89 | return (type, template); 90 | }) 91 | .ToDictionary(o => o.type, o => o.template); 92 | } 93 | 94 | /// 95 | /// Returns the template that best matches the specified type. 96 | /// 97 | private Template FindTemplateForType(Type templateType) 98 | { 99 | var possibleMatches = FindTypesMatchingTemplate(templateType); 100 | // .OrderBy(m => m.Key) 101 | // .ToList(); 102 | 103 | if(possibleMatches.Count == 0) 104 | { 105 | throw new TemplateNotFoundException("Could not find any template matching type " + templateType); 106 | } 107 | 108 | if(possibleMatches.Count > 1) 109 | { 110 | throw new AmbiguousTypeException("Multiple templates were found that match type " + templateType 111 | + ". This can be caused by providing multiple templates that match types " + 112 | templateType 113 | + " inherits from the same level. Remove one or provide a template that more specifically matches the type."); 114 | } 115 | 116 | return AvailableTemplates[possibleMatches[0]]; 117 | } 118 | 119 | private static List GetTypeWithInterfaces(Type originalType) 120 | { 121 | var interfaces = originalType.GetInterfaces(); 122 | var result = new List(interfaces.Length + 1) 123 | { 124 | originalType 125 | }; 126 | result.AddRange(interfaces); 127 | return result; 128 | } 129 | 130 | private static List GetBaseTypeWithInterfaces(Type originalType) 131 | { 132 | var interfaces = originalType.GetInterfaces(); 133 | var result = new List(interfaces.Length + 1); 134 | if(originalType.BaseType != null) 135 | { 136 | result.Add(originalType.BaseType); 137 | } 138 | 139 | result.AddRange(interfaces); 140 | return result; 141 | } 142 | 143 | private List FindTypesMatchingTemplate(Type originalType) 144 | { 145 | var result = new List(); 146 | var levelToCheck = GetTypeWithInterfaces(originalType); 147 | 148 | while(levelToCheck.Count > 0) 149 | { 150 | var validTypesList = new List(); 151 | var newLevelToCheck = new List(); 152 | foreach(var type in levelToCheck) 153 | { 154 | newLevelToCheck.AddRange(GetBaseTypeWithInterfaces(type)); 155 | 156 | if(AvailableTemplates.ContainsKey(type)) 157 | { 158 | validTypesList.Add(type); 159 | } 160 | } 161 | 162 | if(validTypesList.Count > 0) 163 | { 164 | return validTypesList; 165 | } 166 | 167 | if(newLevelToCheck.Count > 0) 168 | { 169 | levelToCheck = newLevelToCheck; 170 | } 171 | } 172 | 173 | return result; 174 | } 175 | 176 | protected virtual Template CloneTemplate(Template template) 177 | { 178 | return Instantiate(template, transform); 179 | } 180 | 181 | protected virtual void OnTemplateDestroy(Template template) 182 | { 183 | template.SetBindings(false); 184 | } 185 | 186 | /// 187 | /// Create a clone of the template object and bind it to the specified view model. 188 | /// Place the new object under the parent at the specified index, or 0 if no index 189 | /// is specified. 190 | /// 191 | protected void InstantiateTemplate(object templateViewModel, int index = 0) 192 | { 193 | Assert.IsNotNull(templateViewModel, "Cannot instantiate child with null view model"); 194 | 195 | // Select template. 196 | var selectedTemplate = FindTemplateForType(templateViewModel.GetType()); 197 | var newObject = CloneTemplate(selectedTemplate); 198 | 199 | newObject.transform.SetSiblingIndex(index); 200 | _instantiatedTemplates.Add(templateViewModel, newObject); 201 | 202 | // Set up child bindings before we activate the template object so that they will be configured properly before trying to connect. 203 | newObject.InitChildBindings(templateViewModel); 204 | newObject.gameObject.SetActive(true); 205 | } 206 | 207 | /// 208 | /// Recursively look in the type, interfaces it implements and types it inherits 209 | /// from for a type that matches a template. Also store how many steps away from 210 | /// the specified template the found template was. 211 | /// 212 | // private IEnumerable> FindTypesMatchingTemplate(Type t, int index = 0) 213 | // { 214 | // var baseType = t.BaseType; 215 | // if (baseType != null && !baseType.IsInterface) 216 | // { 217 | // foreach (var type in FindTypesMatchingTemplate(baseType, index + 1)) 218 | // { 219 | // yield return type; 220 | // } 221 | // } 222 | // 223 | // foreach (var interfaceType in t.GetInterfaces()) 224 | // { 225 | // foreach (var type in FindTypesMatchingTemplate(interfaceType, index + 1)) 226 | // { 227 | // yield return type; 228 | // } 229 | // } 230 | // 231 | // if (AvailableTemplates.Keys.Contains(t)) 232 | // { 233 | // yield return new KeyValuePair(index, t); 234 | // } 235 | // } 236 | 237 | /// 238 | /// Destroys the instantiated template associated with the provided object. 239 | /// 240 | protected void DestroyTemplate(object viewModelToDestroy) 241 | { 242 | var template = _instantiatedTemplates[viewModelToDestroy]; 243 | OnTemplateDestroy(template); 244 | _instantiatedTemplates.Remove(viewModelToDestroy); 245 | } 246 | 247 | /// 248 | /// Destroys all instantiated templates. 249 | /// 250 | protected void DestroyAllTemplates() 251 | { 252 | var templates = _instantiatedTemplates.Values.ToList(); 253 | _instantiatedTemplates.Clear(); 254 | 255 | foreach(var template in templates) 256 | { 257 | OnTemplateDestroy(template); 258 | } 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /Runtime/Binding/AbstractTemplateSelector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bdd4994f243a54845b03b9b5252bef5b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/AdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding 4 | { 5 | /// 6 | /// Base class for adapter options. 7 | /// 8 | public abstract class AdapterOptions : ScriptableObject 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Runtime/Binding/AdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1bcafe11ba8818644a977088aeea1865 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 873eaa7364d9ac84cae6386ffe467530 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/AdapterInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | public interface IAdapterInfo 6 | { 7 | string Id { get; } 8 | Type InType { get; } 9 | Type OutType { get; } 10 | Type OptionsType { get; } 11 | 12 | object Convert(object valueIn, object options); 13 | } 14 | 15 | public class AdapterInfo : IAdapterInfo 16 | { 17 | private readonly Func _converter; 18 | 19 | public string Id { get; private set; } 20 | public Type InType { get; private set; } 21 | public Type OutType { get; private set; } 22 | public Type OptionsType { get; private set; } 23 | 24 | public AdapterInfo(Func converter, string id) 25 | { 26 | _converter = converter; 27 | Id = id; 28 | InType = typeof(TIn); 29 | OutType = typeof(TOut); 30 | OptionsType = null; 31 | } 32 | 33 | public object Convert(object valueIn, object options) 34 | { 35 | return _converter((TIn) valueIn); 36 | } 37 | } 38 | 39 | public class AdapterInfo : IAdapterInfo 40 | { 41 | private readonly Func _converter; 42 | 43 | public string Id { get; private set; } 44 | public Type InType { get; private set; } 45 | public Type OutType { get; private set; } 46 | public Type OptionsType { get; private set; } 47 | 48 | public AdapterInfo(Func converter, string id) 49 | { 50 | _converter = converter; 51 | Id = id; 52 | InType = typeof(TIn); 53 | OutType = typeof(TOut); 54 | OptionsType = typeof(TOptions); 55 | } 56 | 57 | public object Convert(object valueIn, object options) 58 | { 59 | return _converter((TIn) valueIn, (TOptions)options); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/AdapterInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a78e3117732f4c0458547dc16b810843 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BaseAdapters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using UnityEngine; 4 | using UnityEngine.UI; 5 | using UnityWeld.Binding.Internal; 6 | 7 | namespace UnityWeld.Binding.Adapters 8 | { 9 | public static class BaseAdapters 10 | { 11 | /// 12 | /// Return inverted bool value 13 | /// 14 | public static bool BoolInversion(bool inValue) 15 | { 16 | return !inValue; 17 | } 18 | 19 | /// 20 | /// Returns either the value of TrueColor from the specified adapter options if 21 | /// the input value was true, or FalseColor if it was false. 22 | /// 23 | public static Color BoolToColor(bool valueIn, BoolToColorAdapterOptions options) 24 | { 25 | return valueIn ? options.TrueColor : options.FalseColor; 26 | } 27 | 28 | /// 29 | /// Returns either the value of TrueColors from the specified adapter options if 30 | /// the input value was true, or FalseColors if it was false. 31 | /// 32 | public static ColorBlock BoolToColorBlock(bool valueIn, BoolToColorBlockAdapterOptions options) 33 | { 34 | return valueIn ? options.TrueColors : options.FalseColors; 35 | } 36 | 37 | /// 38 | /// Adapter for converting from a bool to a string. 39 | /// 40 | public static string BoolToString(bool valueIn, BoolToStringAdapterOptions options) 41 | { 42 | return valueIn ? options.TrueValueString : options.FalseValueString; 43 | } 44 | 45 | /// 46 | /// Adapter that converts a single Color to one of the colors inside a ColorBlock 47 | /// 48 | public static ColorBlock ColorToColorBlock(Color valueIn, ColorToColorBlockAdapterOptions options) 49 | { 50 | var colorBlock = options.DefaultColors; 51 | switch (options.OverrideColor) 52 | { 53 | case ColorToColorBlockAdapterOptions.Role.Disabled: 54 | colorBlock.disabledColor = valueIn; 55 | break; 56 | case ColorToColorBlockAdapterOptions.Role.Highlighed: 57 | colorBlock.highlightedColor = valueIn; 58 | break; 59 | case ColorToColorBlockAdapterOptions.Role.Normal: 60 | colorBlock.normalColor = valueIn; 61 | break; 62 | case ColorToColorBlockAdapterOptions.Role.Pressed: 63 | colorBlock.pressedColor = valueIn; 64 | break; 65 | } 66 | 67 | return colorBlock; 68 | } 69 | 70 | /// 71 | /// Adapter for converting from a DateTime to an OADate as a float. 72 | /// 73 | public static float DateTimeToOADate(DateTime valueIn) 74 | { 75 | return (float)valueIn.ToOADate(); 76 | } 77 | 78 | /// 79 | /// Adapter for converting from a DateTime to a string. 80 | /// 81 | public static string DateTimeToString(DateTime valueIn, DateTimeToStringAdapterOptions options) 82 | { 83 | return valueIn.ToString(options.Format); 84 | } 85 | 86 | /// 87 | /// Adapter for converting from a float as an OADate to a DateTime. 88 | /// 89 | public static DateTime FloatToDateTime(float valueIn) 90 | { 91 | return DateTime.FromOADate(valueIn); 92 | } 93 | 94 | /// 95 | /// Adapter that converts a float to a string. 96 | /// 97 | public static string FloatToString(float valueIn, FloatToStringAdapterOptions options) 98 | { 99 | return valueIn.ToString(options.Format); 100 | } 101 | 102 | /// 103 | /// Adapter for converting from an int to a string. 104 | /// 105 | public static string IntToString(int valueIn) 106 | { 107 | return valueIn.ToString(); 108 | } 109 | 110 | /// 111 | /// Adapter for converting from a string to a DateTime, using a specified culture. 112 | /// 113 | public static DateTime StringCultureToDateTime(string valueIn, StringCultureToDateTimeAdapterOptions options) 114 | { 115 | var culture = new CultureInfo(options.CultureName); 116 | return DateTime.Parse(valueIn, culture); 117 | } 118 | 119 | /// 120 | /// String to bool adapter that returns false if the string is null or empty, 121 | /// otherwise true. 122 | /// 123 | public static bool StringEmptyToBool(string valueIn) 124 | { 125 | return !string.IsNullOrEmpty(valueIn); 126 | } 127 | 128 | /// 129 | /// Adapter that parses a string as a float. 130 | /// 131 | public static float StringToFloat(string valueIn) 132 | { 133 | return float.Parse(valueIn); 134 | } 135 | 136 | /// 137 | /// Adapter for converting from a string to an int. 138 | /// 139 | public static int StringToInt(string valueIn) 140 | { 141 | return int.Parse(valueIn); 142 | } 143 | 144 | public static void RegisterBaseAdapters() 145 | { 146 | TypeResolver.RegisterAdapter(new AdapterInfo(BoolInversion, nameof(BoolInversion))); 147 | TypeResolver.RegisterAdapter(new AdapterInfo(BoolToColor, nameof(BoolToColor))); 148 | TypeResolver.RegisterAdapter(new AdapterInfo(BoolToColorBlock, nameof(BoolToColorBlock))); 149 | TypeResolver.RegisterAdapter(new AdapterInfo(BoolToString, nameof(BoolToString))); 150 | TypeResolver.RegisterAdapter(new AdapterInfo(ColorToColorBlock, nameof(ColorToColorBlock))); 151 | TypeResolver.RegisterAdapter(new AdapterInfo(DateTimeToOADate, nameof(DateTimeToOADate))); 152 | TypeResolver.RegisterAdapter(new AdapterInfo(DateTimeToString, nameof(DateTimeToString))); 153 | TypeResolver.RegisterAdapter(new AdapterInfo(FloatToDateTime, nameof(FloatToDateTime))); 154 | TypeResolver.RegisterAdapter(new AdapterInfo(FloatToString, nameof(FloatToString))); 155 | TypeResolver.RegisterAdapter(new AdapterInfo(IntToString, nameof(IntToString))); 156 | TypeResolver.RegisterAdapter(new AdapterInfo(StringCultureToDateTime, nameof(StringCultureToDateTime))); 157 | TypeResolver.RegisterAdapter(new AdapterInfo(StringEmptyToBool, nameof(StringEmptyToBool))); 158 | TypeResolver.RegisterAdapter(new AdapterInfo(StringToFloat, nameof(StringToFloat))); 159 | TypeResolver.RegisterAdapter(new AdapterInfo(StringToInt, nameof(StringToInt))); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BaseAdapters.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 11b125bd4fcd32544a4529539831abb0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | /// 6 | /// Options for converting from a bool to a Unity color. 7 | /// 8 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to Color adapter options")] 9 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 10 | public class BoolToColorAdapterOptions : AdapterOptions 11 | { 12 | /// 13 | /// The value used when the bool is false. 14 | /// 15 | public Color FalseColor; 16 | 17 | /// 18 | /// The value used when the bool is true. 19 | /// 20 | public Color TrueColor; 21 | } 22 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2c67f699f0789684ea6213979b0827dc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace UnityWeld.Binding.Adapters 5 | { 6 | /// 7 | /// Options for converting from a bool to a Unity color. 8 | /// 9 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to ColorBlock adapter options")] 10 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 11 | public class BoolToColorBlockAdapterOptions : AdapterOptions 12 | { 13 | /// 14 | /// The value used when the bool is false. 15 | /// 16 | [Header("False colors")] 17 | public ColorBlock FalseColors; 18 | 19 | /// 20 | /// The value used when the bool is true. 21 | /// 22 | [Header("True colors")] 23 | public ColorBlock TrueColors; 24 | } 25 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9283b532ed1dea84a92d639815719846 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | /// 6 | /// Options for converting from a bool to a string. 7 | /// 8 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to string adapter")] 9 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 10 | public class BoolToStringAdapterOptions : AdapterOptions 11 | { 12 | /// 13 | /// The value used when the bool is set to true. 14 | /// 15 | public string TrueValueString; 16 | 17 | /// 18 | /// The value used when the bool is set to false. 19 | /// 20 | public string FalseValueString; 21 | } 22 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1f7217b080af9534dbc6da25f5d7d0a2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace UnityWeld.Binding.Adapters 5 | { 6 | /// 7 | /// Adapter that converts a single Color to one of the colors inside a ColorBlock 8 | /// 9 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Color to ColorBlock adapter")] 10 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 11 | public class ColorToColorBlockAdapterOptions : AdapterOptions 12 | { 13 | public enum Role 14 | { 15 | Disabled, 16 | Highlighed, 17 | Normal, 18 | Pressed 19 | } 20 | 21 | /// 22 | /// Which color to override. 23 | /// 24 | public Role OverrideColor; 25 | 26 | /// 27 | /// Default colors for the other roles. 28 | /// 29 | public ColorBlock DefaultColors; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 799d5e0c6bc32b34791864bff0182127 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | /// 6 | /// Options for converting a DateTime to a string. 7 | /// 8 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/DateTime to string adapter")] 9 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 10 | public class DateTimeToStringAdapterOptions : AdapterOptions 11 | { 12 | /// 13 | /// Format passed in to the DateTime.ToString method. 14 | /// See this page for details on usage: https://msdn.microsoft.com/en-us/library/zdtaw1bw(v=vs.110).aspx 15 | /// 16 | public string Format; 17 | } 18 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4b22843ef765812418e95092ce894efd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | /// 6 | /// Options for the float to string adapter. 7 | /// 8 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Float to string adapter")] 9 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 10 | public class FloatToStringAdapterOptions : AdapterOptions 11 | { 12 | /// 13 | /// Options passed in to the Single.ToString() method. 14 | /// Defaults to two decimal places. 15 | /// See this page for more details: https://msdn.microsoft.com/en-us/library/f71z6k0c(v=vs.110).aspx 16 | /// 17 | public string Format = "0.00"; 18 | } 19 | } -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 05fbf0bb044d2cb48ab5fb79aa4dc01c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityWeld.Binding.Adapters 4 | { 5 | /// 6 | /// Options for converting from a string to a DateTime using format from a specified 7 | /// culture. 8 | /// 9 | [CreateAssetMenu(menuName = "Unity Weld/Adapter options/String to DateTime adapter (using culture specifier)")] 10 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 11 | public class StringCultureToDateTimeAdapterOptions : AdapterOptions 12 | { 13 | public string CultureName; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0237eab34a897cd44a3e5a7ee23180e7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/AnimatorParameterBinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.Assertions; 4 | using UnityEngine.Serialization; 5 | using UnityWeld.Binding.Internal; 6 | 7 | namespace UnityWeld.Binding 8 | { 9 | /// 10 | /// Bind a property in the view model to a parameter in an Animator, subscribing to OnPropertyChanged 11 | /// and updating the Animator parameter accordingly (note that this does not update the view model when 12 | /// the parameter changes). 13 | /// 14 | [RequireComponent(typeof(Animator))] 15 | [AddComponentMenu("Unity Weld/Animator Parameter Binding")] 16 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 17 | public class AnimatorParameterBinding : AbstractMemberBinding 18 | { 19 | /// 20 | /// Type of the adapter we're using to adapt between the view model property and UI property. 21 | /// 22 | public string ViewAdapterId 23 | { 24 | get { return viewAdapterId; } 25 | set { viewAdapterId = value; } 26 | } 27 | 28 | [FormerlySerializedAs("viewAdapterTypeName")] [SerializeField] 29 | private string viewAdapterId; 30 | 31 | /// 32 | /// Options for adapting from the view model to the UI property. 33 | /// 34 | public AdapterOptions ViewAdapterOptions 35 | { 36 | get { return viewAdapterOptions; } 37 | set { viewAdapterOptions = value; } 38 | } 39 | 40 | [SerializeField] 41 | private AdapterOptions viewAdapterOptions; 42 | 43 | /// 44 | /// Name of the property in the view model to bind. 45 | /// 46 | public string ViewModelPropertyName 47 | { 48 | get { return viewModelPropertyName; } 49 | set { viewModelPropertyName = value; } 50 | } 51 | 52 | [SerializeField] 53 | private string viewModelPropertyName; 54 | 55 | /// 56 | /// Parameter name on the Animator 57 | /// 58 | public string AnimatorParameterName 59 | { 60 | get { return animatorParameterName; } 61 | set { animatorParameterName = value; } 62 | } 63 | [SerializeField] 64 | private string animatorParameterName; 65 | 66 | /// 67 | /// The parameter type that we are binding to 68 | /// 69 | public AnimatorControllerParameterType AnimatorParameterType 70 | { 71 | get { return animatorParameterType; } 72 | set { animatorParameterType = value; } 73 | } 74 | [SerializeField] 75 | private AnimatorControllerParameterType animatorParameterType; 76 | 77 | /// 78 | /// Watches the view-model for changes that must be propagated to the view. 79 | /// 80 | private PropertyWatcher viewModelWatcher; 81 | 82 | /// 83 | /// Animator to use 84 | /// 85 | private Animator boundAnimator; 86 | 87 | //Properties to bind to 88 | public float FloatParameter 89 | { 90 | set 91 | { 92 | if(boundAnimator != null) 93 | { 94 | boundAnimator.SetFloat(AnimatorParameterName, value); 95 | } 96 | } 97 | } 98 | 99 | public int IntParameter 100 | { 101 | set 102 | { 103 | if(boundAnimator != null) 104 | { 105 | boundAnimator.SetInteger(AnimatorParameterName, value); 106 | } 107 | } 108 | } 109 | 110 | public bool BoolParameter 111 | { 112 | set 113 | { 114 | if(boundAnimator != null) 115 | { 116 | boundAnimator.SetBool(AnimatorParameterName, value); 117 | } 118 | } 119 | } 120 | 121 | public bool TriggerParameter 122 | { 123 | set 124 | { 125 | if (boundAnimator != null) 126 | { 127 | if (value) 128 | { 129 | boundAnimator.SetTrigger(AnimatorParameterName); 130 | } 131 | else 132 | { 133 | boundAnimator.ResetTrigger(AnimatorParameterName); 134 | } 135 | } 136 | } 137 | } 138 | 139 | public override void Connect() 140 | { 141 | if (boundAnimator == null) 142 | { 143 | boundAnimator = GetComponent(); 144 | } 145 | 146 | Assert.IsTrue( 147 | boundAnimator != null, 148 | "Animator is null!" 149 | ); 150 | 151 | Assert.IsTrue( 152 | !string.IsNullOrEmpty(AnimatorParameterName), 153 | "AnimatorParameter is not set" 154 | ); 155 | 156 | string propertyName; 157 | switch (AnimatorParameterType) 158 | { 159 | case AnimatorControllerParameterType.Float: 160 | propertyName = "FloatParameter"; 161 | break; 162 | case AnimatorControllerParameterType.Int: 163 | propertyName = "IntParameter"; 164 | break; 165 | case AnimatorControllerParameterType.Bool: 166 | propertyName = "BoolParameter"; 167 | break; 168 | case AnimatorControllerParameterType.Trigger: 169 | propertyName = "TriggerParameter"; 170 | break; 171 | default: 172 | throw new IndexOutOfRangeException("Unexpected animator parameter type"); 173 | } 174 | 175 | var viewModelEndPoint = MakeViewModelEndPoint(viewModelPropertyName, null, null); 176 | 177 | // If the binding property is an AnimatorParameterTrigger, 178 | // we change the owner to the instance of the property 179 | // and change the property to "TriggerSetOrReset" 180 | if (AnimatorParameterType == AnimatorControllerParameterType.Trigger) 181 | { 182 | viewModelEndPoint = new PropertyEndPoint(viewModelEndPoint.GetValue(), "TriggerSetOrReset", null, null, "view-model", this); 183 | } 184 | 185 | var propertySync = new PropertySync( 186 | // Source 187 | viewModelEndPoint, 188 | 189 | // Dest 190 | new PropertyEndPoint( 191 | this, 192 | propertyName, 193 | TypeResolver.GetAdapter(viewAdapterId), 194 | viewAdapterOptions, 195 | "Animator", 196 | this 197 | ), 198 | 199 | // Errors, exceptions and validation. 200 | null, // Validation not needed 201 | 202 | this 203 | ); 204 | 205 | viewModelWatcher = viewModelEndPoint.Watch( 206 | () => propertySync.SyncFromSource() 207 | ); 208 | 209 | // Copy the initial value over from the view-model. 210 | propertySync.SyncFromSource(); 211 | } 212 | 213 | public override void Disconnect() 214 | { 215 | if (viewModelWatcher != null) 216 | { 217 | viewModelWatcher.Dispose(); 218 | viewModelWatcher = null; 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Runtime/Binding/AnimatorParameterBinding.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45fb8cae4c29b6841b9b92bae576adda 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/AnimatorParameterTrigger.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace UnityWeld.Binding 4 | { 5 | public class AnimatorParameterTrigger : INotifyPropertyChanged 6 | { 7 | public event PropertyChangedEventHandler PropertyChanged; 8 | 9 | public bool TriggerSetOrReset { private set; get; } 10 | 11 | public AnimatorParameterTrigger(bool defaultTriggerValue) 12 | { 13 | TriggerSetOrReset = defaultTriggerValue; 14 | } 15 | 16 | public void Set() 17 | { 18 | TriggerSetOrReset = true; 19 | OnPropertyChanged("TriggerSetOrReset"); 20 | } 21 | 22 | public void Reset() 23 | { 24 | TriggerSetOrReset = false; 25 | OnPropertyChanged("TriggerSetOrReset"); 26 | } 27 | 28 | protected void OnPropertyChanged(string propertyName) 29 | { 30 | if (PropertyChanged != null) 31 | { 32 | PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Runtime/Binding/AnimatorParameterTrigger.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0912b8bc775e4d9468a9bd10b34ac65b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/BindingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityWeld.Binding 4 | { 5 | /// 6 | /// Mark a class, interface, method or property as bindable. Bindable methods and properties must 7 | /// reside within classes or interfaces that have also been marked as bindable. 8 | /// 9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Interface, 10 | Inherited = false)] 11 | public class BindingAttribute : Attribute 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Runtime/Binding/BindingAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13db563d3ec5acc4f87ae93cadfd5203 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/BindingHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace UnityWeld.Binding 7 | { 8 | public interface IComponentsCache : IDisposable{} 9 | 10 | public interface IComponentsCache : IComponentsCache 11 | { 12 | IList Components { get; } 13 | } 14 | 15 | public static class BindingHelper 16 | { 17 | private class ComponentsCache : IComponentsCache 18 | { 19 | public IList Components { get; } 20 | public Type Type { get; } 21 | 22 | public ComponentsCache(IList cache) 23 | { 24 | Components = cache; 25 | Type = typeof(T); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | Components.Clear(); 31 | InternalCache[Type].Enqueue((IList)Components); 32 | } 33 | } 34 | 35 | private static readonly Dictionary> InternalCache = new Dictionary>(); 36 | 37 | public static IComponentsCache GetComponentsWithCache(this GameObject gameObject, bool withChildren = true) 38 | where T : Component 39 | { 40 | if(gameObject == null) 41 | { 42 | throw new ArgumentNullException(nameof(gameObject)); 43 | } 44 | 45 | var type = typeof(T); 46 | 47 | List cache; 48 | if(!InternalCache.TryGetValue(type, out var cacheQueue)) 49 | { 50 | InternalCache.Add(type, cacheQueue = new Queue()); 51 | } 52 | 53 | if(cacheQueue.Count == 0) 54 | { 55 | cache = new List(100); 56 | } 57 | else 58 | { 59 | cache = (List)cacheQueue.Dequeue(); 60 | cache.Clear(); 61 | } 62 | 63 | if(withChildren) 64 | { 65 | gameObject.GetComponentsInChildren(true, cache); 66 | } 67 | else 68 | { 69 | gameObject.GetComponents(cache); 70 | } 71 | 72 | return new ComponentsCache(cache); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Runtime/Binding/BindingHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ee47ae8bc4c72af478d3b7dfb4fe92c7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/BoundObservableList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Collections.Specialized; 5 | using System.Linq; 6 | 7 | 8 | namespace UnityWeld.Binding 9 | { 10 | /// 11 | /// An observable list that is bound to source list. 12 | /// 13 | public class BoundObservableList : ObservableCollection, IDisposable 14 | { 15 | /// 16 | /// The source list. 17 | /// 18 | private readonly ObservableCollection source; 19 | 20 | /// 21 | /// Function that maps source items to dest items. 22 | /// 23 | private readonly Func itemMap; 24 | 25 | /// 26 | /// Callback when new items are added. 27 | /// 28 | private readonly Action added; 29 | 30 | /// 31 | /// Callback when items are removed. 32 | /// 33 | private readonly Action removed; 34 | 35 | /// 36 | /// Callback invoked when the collection has changed. 37 | /// 38 | private readonly Action changed; 39 | 40 | /// 41 | /// Cache that mimics the contents of the bound list. 42 | /// This is so we know the items that were cleared when the list is reset. 43 | /// 44 | private readonly List cache; 45 | 46 | private bool disposed; 47 | 48 | public BoundObservableList(ObservableCollection source, Func itemMap) : 49 | base(source.Select(itemMap)) 50 | { 51 | this.itemMap = itemMap; 52 | this.source = source; 53 | 54 | source.CollectionChanged += source_CollectionChanged; 55 | CollectionChanged += BoundObservableList_CollectionChanged; 56 | 57 | cache = new List(this); 58 | } 59 | 60 | public BoundObservableList(ObservableCollection source, Func itemMap, Action added, Action removed) : 61 | base(source.Select(itemMap)) 62 | { 63 | if (added == null) 64 | { 65 | throw new ArgumentNullException("added", "added must not be null."); 66 | } 67 | if (removed == null) 68 | { 69 | throw new ArgumentNullException("removed", "removed must not be null."); 70 | } 71 | 72 | this.itemMap = itemMap; 73 | this.source = source; 74 | this.added = added; 75 | this.removed = removed; 76 | 77 | foreach (var item in this) 78 | { 79 | added(item); 80 | } 81 | 82 | source.CollectionChanged += source_CollectionChanged; 83 | CollectionChanged += BoundObservableList_CollectionChanged; 84 | cache = new List(this); 85 | } 86 | 87 | public BoundObservableList(ObservableCollection source, Func itemMap, Action added, Action removed, Action changed) : 88 | base(source.Select(itemMap)) 89 | { 90 | if (added == null) 91 | { 92 | throw new ArgumentNullException("added", "added must not be null."); 93 | } 94 | if (removed == null) 95 | { 96 | throw new ArgumentNullException("removed", "removed must not be null."); 97 | } 98 | 99 | this.itemMap = itemMap; 100 | this.source = source; 101 | this.added = added; 102 | this.removed = removed; 103 | this.changed = changed; 104 | 105 | foreach (var item in this) 106 | { 107 | added(item); 108 | } 109 | 110 | source.CollectionChanged += source_CollectionChanged; 111 | CollectionChanged += BoundObservableList_CollectionChanged; 112 | cache = new List(this); 113 | } 114 | 115 | /// 116 | /// Event raised when the source collection has changed. 117 | /// 118 | private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 119 | { 120 | switch (e.Action) 121 | { 122 | case NotifyCollectionChangedAction.Add: 123 | var insertAt = e.NewStartingIndex; 124 | 125 | foreach (var item in e.NewItems) 126 | { 127 | var generatedItem = itemMap((SourceT)item); 128 | 129 | Insert(insertAt, generatedItem); 130 | ++insertAt; 131 | } 132 | 133 | break; 134 | case NotifyCollectionChangedAction.Remove: 135 | var removeAt = e.OldStartingIndex; 136 | 137 | for (var i = 0; i < e.OldItems.Count; i++) 138 | { 139 | RemoveAt(removeAt); 140 | } 141 | 142 | break; 143 | case NotifyCollectionChangedAction.Reset: 144 | Clear(); 145 | break; 146 | default: 147 | throw new ArgumentOutOfRangeException(); 148 | } 149 | } 150 | 151 | /// 152 | /// Event raised when items are added to the bound list. 153 | /// 154 | private void BoundObservableList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 155 | { 156 | switch (e.Action) 157 | { 158 | case NotifyCollectionChangedAction.Add: 159 | var insertIndex = e.NewStartingIndex; 160 | 161 | foreach (var item in e.NewItems) 162 | { 163 | var typedItem = (DestT)item; 164 | 165 | if (added != null) 166 | { 167 | added(typedItem); 168 | } 169 | 170 | cache.Insert(insertIndex, typedItem); // Keep the cache updated as new items come in. 171 | ++insertIndex; 172 | } 173 | break; 174 | 175 | case NotifyCollectionChangedAction.Remove: 176 | foreach (var item in e.OldItems) 177 | { 178 | var typedItem = (DestT)item; 179 | 180 | if (removed != null) 181 | { 182 | removed(typedItem); 183 | } 184 | 185 | cache.RemoveAt(e.OldStartingIndex); // Keep the cache updated as items are removed. 186 | } 187 | break; 188 | 189 | case NotifyCollectionChangedAction.Reset: 190 | if (removed != null) 191 | { 192 | foreach (var item in cache) 193 | { 194 | removed(item); 195 | } 196 | } 197 | cache.Clear(); 198 | break; 199 | 200 | default: 201 | throw new ArgumentOutOfRangeException(); 202 | } 203 | 204 | if (changed != null) 205 | { 206 | changed.Invoke(); 207 | } 208 | } 209 | 210 | public void Dispose() 211 | { 212 | Dispose(true); 213 | GC.SuppressFinalize(this); 214 | } 215 | 216 | protected virtual void Dispose(bool disposing) 217 | { 218 | if (disposed) 219 | { 220 | return; 221 | } 222 | 223 | if (disposing) 224 | { 225 | source.CollectionChanged -= source_CollectionChanged; 226 | CollectionChanged -= BoundObservableList_CollectionChanged; 227 | } 228 | 229 | disposed = true; 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Runtime/Binding/BoundObservableList.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9422c5ec2af8a964f912e9ba4ccff225 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Binding/CollectionBinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.Linq; 6 | using UnityEngine; 7 | using UnityWeld.Binding.Exceptions; 8 | using UnityWeld.Binding.Internal; 9 | 10 | namespace UnityWeld.Binding 11 | { 12 | /// 13 | /// Binds a property in the view-model that is a collection and instantiates copies 14 | /// of template objects to bind to the items of the collection. 15 | /// 16 | /// Creates and destroys child objects when items are added and removed from a 17 | /// collection that implements INotifyCollectionChanged, like ObservableList. 18 | /// 19 | [AddComponentMenu("Unity Weld/Collection Binding")] 20 | [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] 21 | public class CollectionBinding : AbstractTemplateSelector 22 | { 23 | private readonly IDictionary> _pool = new Dictionary>(); 24 | 25 | /// 26 | /// Collection that we have bound to. 27 | /// 28 | private IEnumerable _viewModelCollectionValue; 29 | 30 | [SerializeField] 31 | private Transform _itemsContainer; 32 | [SerializeField] 33 | private int _templateInitialPoolCount = 0; 34 | 35 | public Transform ItemsContainer 36 | { 37 | get => _itemsContainer; 38 | set => _itemsContainer = value; 39 | } 40 | 41 | public int TemplateInitialPoolCount 42 | { 43 | get => _templateInitialPoolCount; 44 | set => _templateInitialPoolCount = value; 45 | } 46 | 47 | protected Transform Container => _itemsContainer ? _itemsContainer : transform; 48 | 49 | public override void Connect() 50 | { 51 | Disconnect(); 52 | 53 | ParseViewModelEndPointReference( 54 | ViewModelPropertyName, 55 | out var propertyName, 56 | out var newViewModel 57 | ); 58 | 59 | ViewModel = newViewModel; 60 | 61 | ViewModelPropertyWatcher = new PropertyWatcher( 62 | newViewModel, 63 | propertyName, 64 | NotifyPropertyChanged_PropertyChanged 65 | ); 66 | 67 | BindCollection(); 68 | } 69 | 70 | public override void Disconnect() 71 | { 72 | UnbindCollection(); 73 | 74 | if (ViewModelPropertyWatcher != null) 75 | { 76 | ViewModelPropertyWatcher.Dispose(); 77 | ViewModelPropertyWatcher = null; 78 | } 79 | 80 | ViewModel = null; 81 | } 82 | 83 | private void NotifyPropertyChanged_PropertyChanged() 84 | { 85 | RebindCollection(); 86 | } 87 | 88 | private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 89 | { 90 | switch (e.Action) 91 | { 92 | case NotifyCollectionChangedAction.Add: 93 | // Add items that were added to the bound collection. 94 | if (e.NewItems != null) 95 | { 96 | var list = _viewModelCollectionValue as IList; 97 | 98 | foreach (var item in e.NewItems) 99 | { 100 | int index; 101 | if (list == null) 102 | { 103 | // Default to adding the new object at the last index. 104 | index = transform.childCount; 105 | } 106 | else 107 | { 108 | index = list.IndexOf(item); 109 | } 110 | InstantiateTemplate(item, index); 111 | } 112 | } 113 | 114 | break; 115 | case NotifyCollectionChangedAction.Remove: 116 | // TODO: respect item order 117 | // Remove items that have been deleted. 118 | if (e.OldItems != null) 119 | { 120 | foreach (var item in e.OldItems) 121 | { 122 | DestroyTemplate(item); 123 | } 124 | } 125 | 126 | break; 127 | case NotifyCollectionChangedAction.Reset: 128 | DestroyAllTemplates(); 129 | break; 130 | default: 131 | throw new ArgumentOutOfRangeException(); 132 | } 133 | } 134 | 135 | /// 136 | /// Bind to the view model collection so we can monitor it for changes. 137 | /// 138 | private void BindCollection() 139 | { 140 | // Bind view model. 141 | var viewModelType = ViewModel.GetType(); 142 | 143 | ParseEndPointReference(ViewModelPropertyName, out var propertyName, out _); 144 | 145 | var viewModelCollectionProperty = viewModelType.GetProperty(propertyName); 146 | if (viewModelCollectionProperty == null) 147 | { 148 | throw new MemberNotFoundException( 149 | $"Expected property {ViewModelPropertyName}, but it wasn't found on type {viewModelType}."); 150 | } 151 | 152 | // Get value from view model. 153 | var viewModelValue = viewModelCollectionProperty.GetValue(ViewModel, null); 154 | if (viewModelValue == null) 155 | { 156 | return; 157 | } 158 | 159 | _viewModelCollectionValue = viewModelValue as IEnumerable; 160 | if (_viewModelCollectionValue == null) 161 | { 162 | throw new InvalidTypeException( 163 | $"Property {ViewModelPropertyName} is not a collection and cannot be used to bind collections."); 164 | } 165 | 166 | // Generate children 167 | var collectionAsList = _viewModelCollectionValue.Cast().ToList(); 168 | for (var index = 0; index < collectionAsList.Count; index++) 169 | { 170 | InstantiateTemplate(collectionAsList[index], index); 171 | } 172 | 173 | // Subscribe to collection changed events. 174 | if (_viewModelCollectionValue is INotifyCollectionChanged collectionChanged) 175 | { 176 | collectionChanged.CollectionChanged += Collection_CollectionChanged; 177 | } 178 | } 179 | 180 | /// 181 | /// Unbind from the collection, stop monitoring it for changes. 182 | /// 183 | private void UnbindCollection() 184 | { 185 | DestroyAllTemplates(); 186 | 187 | // Unsubscribe from collection changed events. 188 | if (_viewModelCollectionValue != null) 189 | { 190 | var collectionChanged = _viewModelCollectionValue as INotifyCollectionChanged; 191 | if (collectionChanged != null) 192 | { 193 | collectionChanged.CollectionChanged -= Collection_CollectionChanged; 194 | } 195 | 196 | _viewModelCollectionValue = null; 197 | } 198 | } 199 | 200 | /// 201 | /// Rebind to the collection when it has changed on the view-model. 202 | /// 203 | private void RebindCollection() 204 | { 205 | UnbindCollection(); 206 | BindCollection(); 207 | } 208 | 209 | protected override Template CloneTemplate(Template template) 210 | { 211 | if(_pool.TryGetValue(template.ViewModelTypeName, out var pool) && pool.Count > 0) 212 | { 213 | return pool.Dequeue(); 214 | } 215 | 216 | return NewTemplate(template); 217 | } 218 | 219 | protected override void OnTemplateDestroy(Template template) 220 | { 221 | base.OnTemplateDestroy(template); 222 | 223 | PutTemplateToPool(template); 224 | } 225 | 226 | private void PutTemplateToPool(Template template) 227 | { 228 | if(!_pool.TryGetValue(template.ViewModelTypeName, out var pool)) 229 | { 230 | _pool.Add(template.ViewModelTypeName, pool = new Queue