Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9529

2つのObservableCollectionの双方向同期

$
0
0

概要

2つのObservableCollectionを双方向に同期させたいことがあります。
以下のGIFは、左にObservableCollection<int>、右にObservableCollection<string>がBindingされています。2つのObservableCollectionを双方向に同期させて、どちらを変更しても、もう片方に反映されるデモです。

demo.gif

デモはWPFで作成しましたが、ObservableCollection自体はWPFには依存しないので、UWPでもコンソールでも使えます。

コード

2つのObservableCollectionを同期するために、両方のCollectionChangedイベントを購読して、相手側を変更しています。
無限ループを避けるため、ローカル変数のisChangingで既に変更中かの状態を保持しています。

publicstaticclassObservableCollectionExtension{/// <summary>/// 指定したコレクションからコピーされた要素を格納するObservableCollectionを生成/// </summary>publicstaticObservableCollection<T>ToObservableCollection<T>(thisIEnumerable<T>source)=>newObservableCollection<T>(source);/// <summary>/// 指定したObservableCollectionと双方向に同期したObservableCollectionを生成する/// </summary>publicstaticObservableCollection<TargetT>ToObservableCollctionSynced<SourceT,TargetT>(thisObservableCollection<SourceT>sources,Func<SourceT,TargetT>sourceToTarget,Func<TargetT,SourceT>targetToSource){//sourcesの要素を変換したコレクションを生成vartargets=sources.Select(sourceToTarget).ToObservableCollection();//2つのコレクションを同期させるSyncCollectionTwoWay(sources,targets,sourceToTarget,targetToSource);//同期済みのコレクションを返すreturntargets;}/// <summary>/// 2つのObservableCollectionを双方向に同期させる/// </summary>publicstaticvoidSyncCollectionTwoWay<SourceT,TargetT>(ObservableCollection<SourceT>sources,ObservableCollection<TargetT>targets,Func<SourceT,TargetT>sourceToTarget,Func<TargetT,SourceT>targetToSource){boolisChanging=false;//Source -> Targetsources.CollectionChanged+=(o,e)=>ExcuteIfNotChanging(()=>SyncByChangedEventArgs(sources,targets,sourceToTarget,e));//Target -> Sourcetargets.CollectionChanged+=(o,e)=>ExcuteIfNotChanging(()=>SyncByChangedEventArgs(targets,sources,targetToSource,e));//変更イベントループしてしまわないように、ローカル変数(isChanging)でチェック//ローカル変数(isChanging)にアクセスするため、ローカル関数で記述voidExcuteIfNotChanging(Actionaction){if(isChanging)return;isChanging=true;action.Invoke();isChanging=false;}}privatestaticvoidSyncByChangedEventArgs<OriginT,DestT>(ObservableCollection<OriginT>origin,ObservableCollection<DestT>dest,Func<OriginT,DestT>originToDest,NotifyCollectionChangedEventArgsoriginE){switch(originE.Action){caseNotifyCollectionChangedAction.Add:if(originE.NewItems?[0]isOriginTaddItem)dest.Insert(originE.NewStartingIndex,originToDest(addItem));return;caseNotifyCollectionChangedAction.Remove:if(originE.OldStartingIndex>=0)dest.RemoveAt(originE.OldStartingIndex);return;caseNotifyCollectionChangedAction.Replace:if(originE.NewItems?[0]isOriginTreplaceItem)dest[originE.NewStartingIndex]=originToDest(replaceItem);return;caseNotifyCollectionChangedAction.Move:dest.Move(originE.OldStartingIndex,originE.NewStartingIndex);return;caseNotifyCollectionChangedAction.Reset:dest.Clear();foreach(DestTiteminorigin.Select(originToDest))dest.Add(item);return;}}}

使用方法

使用方法は単純で元となるなるObservableCollectionから拡張メソッドで呼ぶだけです。その際に双方向の要素変換のデリゲートを引数に指定します。
ここではSource->Targetは数字を文字列にして固定文字列を足したもの、Target->Sourceは文字列の3文字目以降を数字に変換したもの、になっています。

classMainWindowViewModel{publicObservableCollection<int>Sources{get;}=newObservableCollection<int>(new[]{10,20,30});publicObservableCollection<string>Targets{get;}publicMainWindowViewModel(){Targets=Sources.ToObservableCollctionSynced(x=>$"C:{x}",x=>int.Parse(x.Substring(2)));}}

デモではViewを直接変更するため、あえてコードビハインドで変更しています。

<Windowx:Class="ObservableColletionSyncedTest.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:ObservableColletionSyncedTest"><Window.DataContext><local:MainWindowViewModel/></Window.DataContext><UniformGridColumns="2"><StackPanel><LabelHorizontalContentAlignment="Center"Content="Source"/><ButtonClick="AddSourceButton_Click"Content="Add"/><ButtonClick="RemoveSourceButton_Click"Content="Remove"/><ButtonClick="ReplaceSourceButton_Click"Content="Replace"/><ButtonClick="MoveSourceButton_Click"Content="Move"/><ButtonClick="ClearSourceButton_Click"Content="Clear"/><ListBoxx:Name="sources"ItemsSource="{Binding Sources}"/></StackPanel><StackPanel><LabelHorizontalContentAlignment="Center"Content="Target"/><ButtonClick="AddTargetButton_Click"Content="Add"/><ButtonClick="RemoveTargetButton_Click"Content="Remove"/><ButtonClick="ReplaceTargetButton_Click"Content="Replace"/><ButtonClick="MoveTargetButton_Click"Content="Move"/><ButtonClick="ClearTargetButton_Click"Content="Clear"/><ListBoxx:Name="targets"ItemsSource="{Binding Targets}"/></StackPanel></UniformGrid></Window>
publicpartialclassMainWindow:Window{publicMainWindow(){InitializeComponent();}Randomrandom=newRandom();ObservableCollection<string>targetItems=>(targets.ItemsSourceasObservableCollection<string>);ObservableCollection<int>sourcesItems=>(sources.ItemsSourceasObservableCollection<int>);privateintCreateSourceValue()=>random.Next(0,99);privateintGetRandomIndex<T>(Collection<T>collection)=>random.Next(0,collection.Count);privatevoidAddSourceButton_Click(objectsender,RoutedEventArgse)=>sourcesItems.Add(CreateSourceValue());privatevoidAddTargetButton_Click(objectsender,RoutedEventArgse)=>targetItems.Add($"A:{CreateSourceValue()}");privatevoidRemoveSourceButton_Click(objectsender,RoutedEventArgse)=>sourcesItems.RemoveAt(GetRandomIndex(sourcesItems));privatevoidRemoveTargetButton_Click(objectsender,RoutedEventArgse)=>targetItems.RemoveAt(GetRandomIndex(targetItems));privatevoidReplaceSourceButton_Click(objectsender,RoutedEventArgse)=>sourcesItems[GetRandomIndex(sourcesItems)]=CreateSourceValue();privatevoidReplaceTargetButton_Click(objectsender,RoutedEventArgse)=>targetItems[GetRandomIndex(targetItems)]=$"R:{CreateSourceValue()}";privatevoidMove<T>(ObservableCollection<T>collection){intindexOld=GetRandomIndex(collection);intindexNew=GetRandomIndex(collection);collection.Move(indexOld,indexNew);}privatevoidMoveSourceButton_Click(objectsender,RoutedEventArgse)=>Move(sourcesItems);privatevoidMoveTargetButton_Click(objectsender,RoutedEventArgse)=>Move(targetItems);privatevoidClearSourceButton_Click(objectsender,RoutedEventArgse)=>sourcesItems.Clear();privatevoidClearTargetButton_Click(objectsender,RoutedEventArgse)=>targetItems.Clear();}

注意点

デモではわかりやすくするため、ObservableCollectionを両方ともViewにBindingしていましたが、この用途なら、どちらかのListBoxに双方向の変換のConverterを挟んだほうがよいです。
実際はModel層とViewModel層のObservableCollectionを同期したい、といった用途が多いと思います。

ColletionChangedイベントの購読を解除する方法は無いため、2つのObservableCollectionの寿命が違う場合はメモリリークします。

参考

http://nomoredeathmarch.hatenablog.com/entry/2019/03/02/180147

全体コード

以下の場所においておきます。
https://github.com/soi013/ObservableColletionSyncedTest/

環境

VisualStudio 2019 Version 16.8.3
.NET Core 3.1
C#8


Viewing all articles
Browse latest Browse all 9529

Trending Articles