ui
This commit is contained in:
parent
890f458b26
commit
501a6c3c0d
101
README.md
101
README.md
@ -1 +1,102 @@
|
||||
# ObservableCollections
|
||||
[](https://github.com/Cysharp/ObservableCollections/actions) [](https://github.com/Cysharp/ObservableCollections/releases)
|
||||
|
||||
ObservableCollections is a high performance observable collections(`ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`) with synchronized views.
|
||||
|
||||
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
|
||||
|
||||
It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
|
||||
|
||||
ObservableCollections introduces generics version of `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`).
|
||||
|
||||
```csharp
|
||||
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
|
||||
|
||||
public readonly ref struct NotifyCollectionChangedEventArgs<T>
|
||||
{
|
||||
public readonly NotifyCollectionChangedAction Action;
|
||||
public readonly bool IsSingleItem;
|
||||
public readonly T NewItem;
|
||||
public readonly T OldItem;
|
||||
public readonly ReadOnlySpan<T> NewItems;
|
||||
public readonly ReadOnlySpan<T> OldItems;
|
||||
public readonly int NewStartingIndex;
|
||||
public readonly int OldStartingIndex;
|
||||
}
|
||||
```
|
||||
|
||||
Also, use the interface `IObservableCollection<T>` instead of `INotifyCollectionChanged`. This is guaranteed to be thread-safe and can produce a View that is fully synchronized with the collection.
|
||||
|
||||
```csharp
|
||||
public interface IObservableCollection<T> : IReadOnlyCollection<T>
|
||||
{
|
||||
event NotifyCollectionChangedEventHandler<T> CollectionChanged;
|
||||
object SyncRoot { get; }
|
||||
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
|
||||
}
|
||||
|
||||
// also exists SortedView
|
||||
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer);
|
||||
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer);
|
||||
```
|
||||
|
||||
SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc.
|
||||
|
||||

|
||||
|
||||
ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `RingBuffer`, especially `FixedSizeRingBuffer`, can be achieved with efficient performance when there is rotation (e.g., displaying up to 1000 logs, where old ones are deleted when new ones are added). Of course, the AddRange allows for efficient batch processing of large numbers of additions.
|
||||
|
||||
Getting Started
|
||||
---
|
||||
For .NET, use NuGet. For Unity, please read [Unity](#unity) section.
|
||||
|
||||
PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
|
||||
|
||||
create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```csharp
|
||||
// WPF simple sample.
|
||||
|
||||
ObservableList<int> list;
|
||||
public ISynchronizedView<int, int> ItemsView { get; set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContext = this;
|
||||
|
||||
list = new ObservableList<int>();
|
||||
list.AddRange(new[] { 1, 10, 188 });
|
||||
ItemsView = list.CreateSortedView(x => x, x => x, comparer: Comparer<int>.Default).WithINotifyCollectionChanged();
|
||||
|
||||
BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ItemsView.Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
TODO: write more usage...
|
||||
|
||||
View/SoretedView
|
||||
---
|
||||
|
||||
|
||||
Collections
|
||||
---
|
||||
|
||||
|
||||
Unity
|
||||
---
|
||||
|
||||
|
||||
License
|
||||
---
|
||||
This library is licensed under the MIT License.
|
BIN
docs/assets.pptx
Normal file
BIN
docs/assets.pptx
Normal file
Binary file not shown.
@ -50,5 +50,10 @@ namespace WpfApp
|
||||
list.Add(adder++);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ItemsView.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
197
src/ObservableCollections.Unity/Assets/Scenes/Button.prefab
Normal file
197
src/ObservableCollections.Unity/Assets/Scenes/Button.prefab
Normal file
@ -0,0 +1,197 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &1956618470265689906
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 618331874408718809}
|
||||
- component: {fileID: 7969674051040476341}
|
||||
- component: {fileID: 3304697755884386642}
|
||||
m_Layer: 5
|
||||
m_Name: Text
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &618331874408718809
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1956618470265689906}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 5255390928395226544}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7969674051040476341
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1956618470265689906}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!114 &3304697755884386642
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1956618470265689906}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_FontSize: 14
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 10
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: Button
|
||||
--- !u!1 &8295906003645589311
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5255390928395226544}
|
||||
- component: {fileID: 8560960181737511060}
|
||||
- component: {fileID: 6330597562278295799}
|
||||
- component: {fileID: 2332625603231720647}
|
||||
m_Layer: 5
|
||||
m_Name: Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5255390928395226544
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8295906003645589311}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 618331874408718809}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 160, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8560960181737511060
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8295906003645589311}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!114 &6330597562278295799
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8295906003645589311}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &2332625603231720647
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8295906003645589311}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 6330597562278295799}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a021aee34791a94f83af413a3ade50c
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,78 @@
|
||||
using ObservableCollections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class SampleScript : MonoBehaviour
|
||||
{
|
||||
public Button prefab;
|
||||
public GameObject root;
|
||||
|
||||
public Button add;
|
||||
public Button remove;
|
||||
|
||||
int i = 0;
|
||||
|
||||
void Start()
|
||||
{
|
||||
var oc = new ObservableRingBuffer<int>();
|
||||
|
||||
var view = oc.CreateView(x =>
|
||||
{
|
||||
var item = GameObject.Instantiate(prefab);
|
||||
item.GetComponentInChildren<Text>().text = x.ToString();
|
||||
return item.gameObject;
|
||||
});
|
||||
view.AttachFilter(new GameObjectFilter(root));
|
||||
|
||||
add.onClick.AddListener(() =>
|
||||
{
|
||||
oc.AddLast(i++);
|
||||
});
|
||||
|
||||
remove.onClick.AddListener(() =>
|
||||
{
|
||||
oc.RemoveFirst();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class GameObjectFilter : ISynchronizedViewFilter<int, GameObject>
|
||||
{
|
||||
readonly GameObject root;
|
||||
|
||||
public GameObjectFilter(GameObject root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public void OnCollectionChanged(ChangedKind changedKind, int value, GameObject view)
|
||||
{
|
||||
if (changedKind == ChangedKind.Add)
|
||||
{
|
||||
view.transform.SetParent(root.transform);
|
||||
}
|
||||
else if (changedKind == ChangedKind.Remove)
|
||||
{
|
||||
GameObject.Destroy(view);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMatch(int value, GameObject view)
|
||||
{
|
||||
return value % 2 == 0;
|
||||
}
|
||||
|
||||
public void WhenTrue(int value, GameObject view)
|
||||
{
|
||||
view.SetActive(true);
|
||||
}
|
||||
|
||||
public void WhenFalse(int value, GameObject view)
|
||||
{
|
||||
view.SetActive(false);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e89d460dcf6b5db44a0e995732078819
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
x
Reference in New Issue
Block a user