Observable collection base code for those without a true backing collection.
Observable sorted dictionary Grid view now displays blocks grouped by subtype. Null propagation in entity view models because WPF.
This commit is contained in:
@@ -217,6 +217,7 @@
|
||||
<Compile Include="ViewModels\Entities\CharacterViewModel.cs" />
|
||||
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
|
||||
<Compile Include="ViewModels\Entities\EntityControlViewModel.cs" />
|
||||
<Compile Include="Views\Converters\DefinitionToIdConverter.cs" />
|
||||
<Compile Include="Views\Entities\EntityControlHost.xaml.cs">
|
||||
<DependentUpon>EntityControlHost.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@@ -15,10 +15,10 @@ namespace Torch.Server.ViewModels.Blocks
|
||||
{
|
||||
public class BlockViewModel : EntityViewModel
|
||||
{
|
||||
public IMyTerminalBlock Block { get; }
|
||||
public IMyTerminalBlock Block => (IMyTerminalBlock) Entity;
|
||||
public MtObservableList<PropertyViewModel> Properties { get; } = new MtObservableList<PropertyViewModel>();
|
||||
|
||||
public string FullName => $"{Block.CubeGrid.CustomName} - {Block.CustomName}";
|
||||
public string FullName => $"{Block?.CubeGrid.CustomName} - {Block?.CustomName}";
|
||||
|
||||
public override string Name
|
||||
{
|
||||
@@ -38,7 +38,7 @@ namespace Torch.Server.ViewModels.Blocks
|
||||
|
||||
public long BuiltBy
|
||||
{
|
||||
get => ((MySlimBlock)Block.SlimBlock).BuiltBy;
|
||||
get => ((MySlimBlock)Block?.SlimBlock)?.BuiltBy ?? 0;
|
||||
set
|
||||
{
|
||||
TorchBase.Instance.Invoke(() =>
|
||||
@@ -59,7 +59,6 @@ namespace Torch.Server.ViewModels.Blocks
|
||||
|
||||
public BlockViewModel(IMyTerminalBlock block, EntityTreeViewModel tree) : base(block, tree)
|
||||
{
|
||||
Block = block;
|
||||
if (Block == null)
|
||||
return;
|
||||
|
||||
|
@@ -32,7 +32,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get => Entity.DisplayName;
|
||||
get => Entity?.DisplayName;
|
||||
set
|
||||
{
|
||||
TorchBase.Instance.InvokeBlocking(() => Entity.DisplayName = value);
|
||||
@@ -42,7 +42,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
|
||||
public virtual string Position
|
||||
{
|
||||
get => Entity.GetPosition().ToString();
|
||||
get => Entity?.GetPosition().ToString();
|
||||
set
|
||||
{
|
||||
if (!Vector3D.TryParse(value, out Vector3D v))
|
||||
|
@@ -1,68 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Sandbox.Definitions;
|
||||
using Sandbox.Game.Entities;
|
||||
using Sandbox.Game.Entities.Cube;
|
||||
using Sandbox.ModAPI;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.ViewModels.Blocks;
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
public class GridViewModel : EntityViewModel, ILazyLoad
|
||||
{
|
||||
private MyCubeGrid Grid => (MyCubeGrid)Entity;
|
||||
public MtObservableList<BlockViewModel> Blocks { get; } = new MtObservableList<BlockViewModel>();
|
||||
private static readonly MyCubeBlockDefinition _fillerDefinition = new MyCubeBlockDefinition()
|
||||
{
|
||||
Id = new MyDefinitionId(typeof(MyObjectBuilder_DefinitionBase), "")
|
||||
};
|
||||
|
||||
private class CubeBlockDefinitionComparer : IComparer<MyCubeBlockDefinition>
|
||||
{
|
||||
public static readonly CubeBlockDefinitionComparer Default = new CubeBlockDefinitionComparer();
|
||||
|
||||
public int Compare(MyCubeBlockDefinition x, MyCubeBlockDefinition y)
|
||||
{
|
||||
if (x == null && y == null)
|
||||
return 0;
|
||||
if (x == null)
|
||||
return -1;
|
||||
if (y == null)
|
||||
return 1;
|
||||
if (ReferenceEquals(x, y))
|
||||
return 0;
|
||||
MyDefinitionId xi = x.Id;
|
||||
MyDefinitionId yi = y.Id;
|
||||
if (xi == yi)
|
||||
return 0;
|
||||
if (xi.TypeId != yi.TypeId)
|
||||
return string.CompareOrdinal(((Type) xi.TypeId).Name, ((Type) yi.TypeId).Name);
|
||||
return xi.SubtypeId == yi.SubtypeId ? 0 : string.CompareOrdinal(xi.SubtypeName, yi.SubtypeName);
|
||||
}
|
||||
}
|
||||
|
||||
private MyCubeGrid Grid => (MyCubeGrid) Entity;
|
||||
|
||||
public MtObservableSortedDictionary<MyCubeBlockDefinition, MtObservableSortedDictionary<long, BlockViewModel>>
|
||||
Blocks { get; } =
|
||||
new MtObservableSortedDictionary<MyCubeBlockDefinition, MtObservableSortedDictionary<long, BlockViewModel>>(
|
||||
CubeBlockDefinitionComparer.Default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DescriptiveName { get; }
|
||||
|
||||
public GridViewModel() { }
|
||||
public GridViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree)
|
||||
{
|
||||
DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)";
|
||||
Blocks.Add(new BlockViewModel(null, Tree));
|
||||
Blocks.Add(_fillerDefinition, new MtObservableSortedDictionary<long, BlockViewModel>());
|
||||
}
|
||||
|
||||
private void Grid_OnBlockRemoved(Sandbox.Game.Entities.Cube.MySlimBlock obj)
|
||||
{
|
||||
if (obj.FatBlock != null)
|
||||
Blocks.RemoveWhere(b => b.Block.EntityId == obj.FatBlock?.EntityId);
|
||||
RemoveBlock(obj.FatBlock);
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
private void RemoveBlock(MyCubeBlock block)
|
||||
{
|
||||
if (!Blocks.TryGetValue(block.BlockDefinition, out var group))
|
||||
return;
|
||||
if (group.Remove(block.EntityId) && group.Count == 0 && Blocks.Count > 1)
|
||||
Blocks.Remove(block.BlockDefinition);
|
||||
}
|
||||
|
||||
private void AddBlock(MyTerminalBlock block)
|
||||
{
|
||||
if (!Blocks.TryGetValue(block.BlockDefinition, out var group))
|
||||
group = Blocks[block.BlockDefinition] = new MtObservableSortedDictionary<long, BlockViewModel>();
|
||||
group.Add(block.EntityId, new BlockViewModel(block, Tree));
|
||||
}
|
||||
|
||||
private void Grid_OnBlockAdded(Sandbox.Game.Entities.Cube.MySlimBlock obj)
|
||||
{
|
||||
var block = obj.FatBlock as IMyTerminalBlock;
|
||||
var block = obj.FatBlock as MyTerminalBlock;
|
||||
if (block != null)
|
||||
Blocks.Add(new BlockViewModel(block, Tree));
|
||||
AddBlock(block);
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
private bool _load;
|
||||
|
||||
public void Load()
|
||||
{
|
||||
if (_load)
|
||||
return;
|
||||
|
||||
_load = true;
|
||||
Blocks.Clear();
|
||||
TorchBase.Instance.Invoke(() =>
|
||||
{
|
||||
foreach (var block in Grid.GetFatBlocks().Where(b => b is IMyTerminalBlock))
|
||||
{
|
||||
Blocks.Add(new BlockViewModel((IMyTerminalBlock)block, Tree));
|
||||
}
|
||||
Blocks.Clear();
|
||||
foreach (var block in Grid.GetFatBlocks().OfType<MyTerminalBlock>())
|
||||
AddBlock(block);
|
||||
|
||||
Grid.OnBlockAdded += Grid_OnBlockAdded;
|
||||
Grid.OnBlockRemoved += Grid_OnBlockRemoved;
|
||||
|
||||
Tree.ControlDispatcher.BeginInvoke(() =>
|
||||
{
|
||||
Blocks.Sort(b => b.Block.CustomName);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -29,11 +29,10 @@ namespace Torch.Server.ViewModels.Entities
|
||||
await TorchBase.Instance.InvokeAsync(() => MyEntities.GetTopMostEntitiesInBox(ref box, entities)).ConfigureAwait(false);
|
||||
foreach (var entity in entities.Where(e => e is IMyCubeGrid))
|
||||
{
|
||||
var gridModel = Tree.Grids.FirstOrDefault(g => g.Entity.EntityId == entity.EntityId);
|
||||
if (gridModel == null)
|
||||
if (Tree.Grids.TryGetValue(entity.EntityId, out var gridModel))
|
||||
{
|
||||
gridModel = new GridViewModel((MyCubeGrid)entity, Tree);
|
||||
Tree.Grids.Add(gridModel);
|
||||
Tree.Grids.Add(entity.EntityId, gridModel);
|
||||
}
|
||||
|
||||
AttachedGrids.Add(gridModel);
|
||||
|
@@ -20,10 +20,10 @@ namespace Torch.Server.ViewModels
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
//TODO: these should be sorted sets for speed
|
||||
public MtObservableList<GridViewModel> Grids { get; set; } = new MtObservableList<GridViewModel>();
|
||||
public MtObservableList<CharacterViewModel> Characters { get; set; } = new MtObservableList<CharacterViewModel>();
|
||||
public MtObservableList<EntityViewModel> FloatingObjects { get; set; } = new MtObservableList<EntityViewModel>();
|
||||
public MtObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableList<VoxelMapViewModel>();
|
||||
public MtObservableSortedDictionary<long, GridViewModel> Grids { get; set; } = new MtObservableSortedDictionary<long, GridViewModel>();
|
||||
public MtObservableSortedDictionary<long, CharacterViewModel> Characters { get; set; } = new MtObservableSortedDictionary<long, CharacterViewModel>();
|
||||
public MtObservableSortedDictionary<long, EntityViewModel> FloatingObjects { get; set; } = new MtObservableSortedDictionary<long, EntityViewModel>();
|
||||
public MtObservableSortedDictionary<long, VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableSortedDictionary<long, VoxelMapViewModel>();
|
||||
public Dispatcher ControlDispatcher => _control.Dispatcher;
|
||||
|
||||
private EntityViewModel _currentEntity;
|
||||
@@ -35,6 +35,11 @@ namespace Torch.Server.ViewModels
|
||||
set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); }
|
||||
}
|
||||
|
||||
// I hate you today WPF
|
||||
public EntityTreeViewModel() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public EntityTreeViewModel(UserControl control)
|
||||
{
|
||||
_control = control;
|
||||
@@ -53,16 +58,16 @@ namespace Torch.Server.ViewModels
|
||||
switch (obj)
|
||||
{
|
||||
case MyCubeGrid grid:
|
||||
Grids.RemoveWhere(m => m.Id == grid.EntityId);
|
||||
Grids.Remove(grid.EntityId);
|
||||
break;
|
||||
case MyCharacter character:
|
||||
Characters.RemoveWhere(m => m.Id == character.EntityId);
|
||||
Characters.Remove(character.EntityId);
|
||||
break;
|
||||
case MyFloatingObject floating:
|
||||
FloatingObjects.RemoveWhere(m => m.Id == floating.EntityId);
|
||||
FloatingObjects.Remove(floating.EntityId);
|
||||
break;
|
||||
case MyVoxelBase voxel:
|
||||
VoxelMaps.RemoveWhere(m => m.Id == voxel.EntityId);
|
||||
VoxelMaps.Remove(voxel.EntityId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -80,16 +85,16 @@ namespace Torch.Server.ViewModels
|
||||
switch (obj)
|
||||
{
|
||||
case MyCubeGrid grid:
|
||||
Grids.Add(new GridViewModel(grid, this));
|
||||
Grids.Add(obj.EntityId, new GridViewModel(grid, this));
|
||||
break;
|
||||
case MyCharacter character:
|
||||
Characters.Add(new CharacterViewModel(character, this));
|
||||
Characters.Add(obj.EntityId, new CharacterViewModel(character, this));
|
||||
break;
|
||||
case MyFloatingObject floating:
|
||||
FloatingObjects.Add(new FloatingObjectViewModel(floating, this));
|
||||
FloatingObjects.Add(obj.EntityId, new FloatingObjectViewModel(floating, this));
|
||||
break;
|
||||
case MyVoxelBase voxel:
|
||||
VoxelMaps.Add(new VoxelMapViewModel(voxel, this));
|
||||
VoxelMaps.Add(obj.EntityId, new VoxelMapViewModel(voxel, this));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
40
Torch.Server/Views/Converters/DefinitionToIdConverter.cs
Normal file
40
Torch.Server/Views/Converters/DefinitionToIdConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using Sandbox.Definitions;
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.Server.Views.Converters
|
||||
{
|
||||
public class DefinitionToIdConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
MyDefinitionId id = ((MyDefinitionBase) value).Id;
|
||||
string typeName = id.TypeId.ToString();
|
||||
if (typeName.StartsWith("MyObjectBuilder_"))
|
||||
typeName = typeName.Substring("MyObjectBuilder_".Length);
|
||||
string subtype = id.SubtypeName;
|
||||
return string.IsNullOrWhiteSpace(subtype) ? typeName : $"{typeName}: {subtype}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
string[] parts = value.ToString().Split(':');
|
||||
Type type;
|
||||
try
|
||||
{
|
||||
type = Type.GetType(parts[0]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
type = Type.GetType("MyObjectBuilder_" + parts[0]);
|
||||
}
|
||||
return MyDefinitionManager.Static.GetDefinition(
|
||||
new MyDefinitionId(type, parts.Length > 1 ? parts[1].Trim() : ""));
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,30 +7,47 @@
|
||||
xmlns:viewModels="clr-namespace:Torch.Server.ViewModels"
|
||||
xmlns:entities="clr-namespace:Torch.Server.ViewModels.Entities"
|
||||
xmlns:blocks="clr-namespace:Torch.Server.ViewModels.Blocks"
|
||||
xmlns:converters="clr-namespace:Torch.Server.Views.Converters"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.DataContext>
|
||||
<viewModels:EntityTreeViewModel />
|
||||
</UserControl.DataContext>
|
||||
<UserControl.Resources>
|
||||
<converters:DefinitionToIdConverter x:Key="DefinitionConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition MinWidth="300" Width="Auto"/>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition MinWidth="300" Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TreeView Grid.Row="0" Margin="3" DockPanel.Dock="Top" SelectedItemChanged="TreeView_OnSelectedItemChanged" TreeViewItem.Expanded="TreeViewItem_OnExpanded">
|
||||
<TreeView Grid.Row="0" Margin="3" DockPanel.Dock="Top" SelectedItemChanged="TreeView_OnSelectedItemChanged"
|
||||
TreeViewItem.Expanded="TreeViewItem_OnExpanded">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:GridViewModel}" ItemsSource="{Binding Blocks}">
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:GridViewModel}"
|
||||
ItemsSource="{Binding Path=Blocks}">
|
||||
<TextBlock Text="{Binding DescriptiveName}" />
|
||||
<HierarchicalDataTemplate.ItemTemplate>
|
||||
<HierarchicalDataTemplate ItemsSource="{Binding Value.Values}">
|
||||
<TextBlock Text="{Binding Path=Key, Converter={StaticResource DefinitionConverter}, Mode=OneWay}" />
|
||||
<HierarchicalDataTemplate.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type blocks:BlockViewModel}">
|
||||
<TextBlock Text="{Binding Path=Name}" />
|
||||
</DataTemplate>
|
||||
</HierarchicalDataTemplate.ItemTemplate>
|
||||
</HierarchicalDataTemplate>
|
||||
</HierarchicalDataTemplate.ItemTemplate>
|
||||
</HierarchicalDataTemplate>
|
||||
<DataTemplate DataType="{x:Type blocks:BlockViewModel}">
|
||||
<TextBlock Text="{Binding Path=Name}" />
|
||||
</DataTemplate>
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:VoxelMapViewModel}" ItemsSource="{Binding AttachedGrids}">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:VoxelMapViewModel}"
|
||||
ItemsSource="{Binding AttachedGrids}">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
<TreeViewItem ItemsSource="{Binding Grids}">
|
||||
<TreeViewItem ItemsSource="{Binding Path=Grids.Values}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding Grids.Count, StringFormat=Grids ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
@@ -45,7 +62,7 @@
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
<TreeViewItem ItemsSource="{Binding VoxelMaps}">
|
||||
<TreeViewItem ItemsSource="{Binding VoxelMaps.Values}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding VoxelMaps.Count, StringFormat=Voxel Maps ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
@@ -55,7 +72,7 @@
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
<TreeViewItem ItemsSource="{Binding FloatingObjects}">
|
||||
<TreeViewItem ItemsSource="{Binding FloatingObjects.Values}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding FloatingObjects.Count, StringFormat=Floating Objects ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.8
|
||||
VisualStudioVersion = 15.0.27004.2006
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch", "Torch\Torch.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}"
|
||||
EndProject
|
||||
@@ -28,6 +28,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning",
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(Performance) = preSolution
|
||||
HasPerformanceSessions = true
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Release|x64 = Release|x64
|
||||
|
@@ -17,56 +17,21 @@ namespace Torch.Collections
|
||||
/// </summary>
|
||||
/// <typeparam name="TC">Collection type</typeparam>
|
||||
/// <typeparam name="TV">Value type</typeparam>
|
||||
public abstract class MtObservableCollection<TC, TV> : INotifyPropertyChanged, INotifyCollectionChanged,
|
||||
IEnumerable<TV>, ICollection where TC : class, ICollection<TV>
|
||||
public abstract class MtObservableCollection<TC, TV> : MtObservableCollectionBase<TV> where TC : class, ICollection<TV>
|
||||
{
|
||||
protected readonly ReaderWriterLockSlim Lock;
|
||||
protected readonly TC Backing;
|
||||
private int _version;
|
||||
private readonly ThreadLocal<ThreadView> _threadViews;
|
||||
protected override ReaderWriterLockSlim Lock { get; }
|
||||
|
||||
protected MtObservableCollection(TC backing)
|
||||
{
|
||||
Backing = backing;
|
||||
// recursion so the events can read snapshots.
|
||||
Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
_version = 0;
|
||||
_threadViews = new ThreadLocal<ThreadView>(() => new ThreadView(this));
|
||||
_deferredSnapshot = new DeferredUpdateToken(this);
|
||||
_flushEventQueue = new Timer(FlushCollectionEventQueue);
|
||||
Backing = backing;
|
||||
}
|
||||
|
||||
~MtObservableCollection()
|
||||
{
|
||||
Timer queue = _flushEventQueue;
|
||||
_flushEventQueue = null;
|
||||
queue?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should this observable collection actually dispatch events.
|
||||
/// </summary>
|
||||
public bool NotificationsEnabled { get; protected set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of this collection. Note: This call is only done when a read lock is acquired.
|
||||
/// </summary>
|
||||
/// <param name="old">Collection to clear and reuse, or null if none</param>
|
||||
/// <returns>The snapshot</returns>
|
||||
protected abstract List<TV> Snapshot(List<TV> old);
|
||||
|
||||
/// <summary>
|
||||
/// Marks all snapshots taken of this collection as dirty.
|
||||
/// </summary>
|
||||
protected void MarkSnapshotsDirty()
|
||||
{
|
||||
_version++;
|
||||
}
|
||||
|
||||
#region ICollection
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(TV item)
|
||||
public override void Add(TV item)
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
@@ -79,7 +44,7 @@ namespace Torch.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
public override void Clear()
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
@@ -91,21 +56,21 @@ namespace Torch.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Contains(TV item)
|
||||
public override bool Contains(TV item)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return Backing.Contains(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyTo(TV[] array, int arrayIndex)
|
||||
public override void CopyTo(TV[] array, int arrayIndex)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
Backing.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(TV item)
|
||||
public override bool Remove(TV item)
|
||||
{
|
||||
using (Lock.UpgradableReadUsing())
|
||||
{
|
||||
@@ -129,7 +94,7 @@ namespace Torch.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count
|
||||
public override int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -139,286 +104,20 @@ namespace Torch.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsReadOnly => Backing.IsReadOnly;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Wrappers
|
||||
|
||||
private readonly DeferredUpdateToken _deferredSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// Disposable that stops update signals and signals a full refresh when disposed.
|
||||
/// </summary>
|
||||
public IDisposable DeferredUpdate()
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
_deferredSnapshot.Enter();
|
||||
return _deferredSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private class DeferredUpdateToken : IDisposable
|
||||
{
|
||||
private readonly MtObservableCollection<TC, TV> _collection;
|
||||
private int _depth;
|
||||
|
||||
internal DeferredUpdateToken(MtObservableCollection<TC, TV> c)
|
||||
{
|
||||
_collection = c;
|
||||
}
|
||||
|
||||
internal void Enter()
|
||||
{
|
||||
if (Interlocked.Increment(ref _depth) == 1)
|
||||
{
|
||||
_collection.NotificationsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _depth) == 0)
|
||||
using (_collection.Lock.WriteUsing())
|
||||
{
|
||||
_collection.NotificationsEnabled = true;
|
||||
_collection.OnPropertyChanged(nameof(Count));
|
||||
_collection.OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged(string propName)
|
||||
{
|
||||
if (!NotificationsEnabled)
|
||||
return;
|
||||
_propertyChangedEvent.Raise(this, new PropertyChangedEventArgs(propName));
|
||||
}
|
||||
|
||||
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (!NotificationsEnabled)
|
||||
return;
|
||||
_collectionEventQueue.Enqueue(e);
|
||||
// In half a second, flush the events
|
||||
_flushEventQueue?.Change(500, -1);
|
||||
}
|
||||
|
||||
private Timer _flushEventQueue;
|
||||
|
||||
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
|
||||
new Queue<NotifyCollectionChangedEventArgs>();
|
||||
|
||||
private void FlushCollectionEventQueue(object data)
|
||||
{
|
||||
// :/, but works better
|
||||
bool reset = _collectionEventQueue.Count > 0;
|
||||
var itemsChanged = false;
|
||||
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
|
||||
if (!reset)
|
||||
{
|
||||
_collectionChangedEvent.Raise(this, e);
|
||||
itemsChanged = true;
|
||||
}
|
||||
|
||||
if (reset)
|
||||
{
|
||||
_collectionChangedEvent.Raise(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
itemsChanged = true;
|
||||
}
|
||||
|
||||
if (itemsChanged)
|
||||
OnPropertyChanged("Item[]");
|
||||
}
|
||||
|
||||
private readonly MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler> _propertyChangedEvent
|
||||
=
|
||||
new MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler>();
|
||||
public override bool IsReadOnly => Backing.IsReadOnly;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
_propertyChangedEvent.Add(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
remove
|
||||
{
|
||||
_propertyChangedEvent.Remove(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>
|
||||
_collectionChangedEvent =
|
||||
new MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
_collectionChangedEvent.Add(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
remove
|
||||
{
|
||||
_collectionChangedEvent.Remove(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Is this collection observed by any listeners.
|
||||
/// </summary>
|
||||
public bool IsObserved => _collectionChangedEvent.IsObserved || _propertyChangedEvent.IsObserved;
|
||||
|
||||
#region Enumeration
|
||||
|
||||
/// <summary>
|
||||
/// Manages a snapshot to a collection and dispatches enumerators from that snapshot.
|
||||
/// </summary>
|
||||
private sealed class ThreadView
|
||||
{
|
||||
private readonly MtObservableCollection<TC, TV> _owner;
|
||||
private readonly WeakReference<List<TV>> _snapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MtObservableCollection{TC,TV}._version"/> of the <see cref="_snapshot"/>
|
||||
/// </summary>
|
||||
private int _snapshotVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Number of strong references to the value pointed to be <see cref="_snapshot"/>
|
||||
/// </summary>
|
||||
private int _snapshotRefCount;
|
||||
|
||||
internal ThreadView(MtObservableCollection<TC, TV> owner)
|
||||
{
|
||||
_owner = owner;
|
||||
_snapshot = new WeakReference<List<TV>>(null);
|
||||
_snapshotVersion = 0;
|
||||
_snapshotRefCount = 0;
|
||||
}
|
||||
|
||||
private List<TV> GetSnapshot()
|
||||
{
|
||||
// reading the version number + snapshots
|
||||
using (_owner.Lock.ReadUsing())
|
||||
{
|
||||
if (!_snapshot.TryGetTarget(out List<TV> currentSnapshot) || _snapshotVersion != _owner._version)
|
||||
{
|
||||
// Update the snapshot, using the old one if it isn't referenced.
|
||||
currentSnapshot = _owner.Snapshot(_snapshotRefCount == 0 ? currentSnapshot : null);
|
||||
_snapshotVersion = _owner._version;
|
||||
_snapshotRefCount = 0;
|
||||
_snapshot.SetTarget(currentSnapshot);
|
||||
}
|
||||
return currentSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrows a snapshot from a <see cref="ThreadView"/> and provides an enumerator.
|
||||
/// Once <see cref="Dispose"/> is called the read lock is released.
|
||||
/// </summary>
|
||||
internal sealed class Enumerator : IEnumerator<TV>
|
||||
{
|
||||
private readonly IEnumerator<TV> _backing;
|
||||
private readonly ThreadView _owner;
|
||||
private bool _disposed;
|
||||
|
||||
internal Enumerator(ThreadView owner)
|
||||
{
|
||||
_owner = owner;
|
||||
// Lock required since destructors run MT
|
||||
lock (_owner)
|
||||
{
|
||||
_owner._snapshotRefCount++;
|
||||
_backing = owner.GetSnapshot().GetEnumerator();
|
||||
}
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
~Enumerator()
|
||||
{
|
||||
// Lock required since destructors run MT
|
||||
if (!_disposed && _owner != null)
|
||||
lock (_owner)
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// safe deref so finalizer can clean up
|
||||
_backing?.Dispose();
|
||||
_owner._snapshotRefCount--;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
return _backing.MoveNext();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
_backing.Reset();
|
||||
}
|
||||
|
||||
public TV Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
return _backing.Current;
|
||||
}
|
||||
}
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<TV> GetEnumerator()
|
||||
{
|
||||
return new ThreadView.Enumerator(_threadViews.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ICollection.CopyTo(Array array, int index)
|
||||
public override void CopyTo(Array array, int index)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
{
|
||||
int i = index;
|
||||
foreach (TV value in Backing)
|
||||
foreach (TV k in Backing)
|
||||
{
|
||||
if (i >= array.Length)
|
||||
break;
|
||||
array.SetValue(value, i++);
|
||||
array.SetValue(k, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool ICollection.IsSynchronized => true;
|
||||
#endregion
|
||||
}
|
||||
}
|
347
Torch/Collections/MtObservableCollectionBase.cs
Normal file
347
Torch/Collections/MtObservableCollectionBase.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Multithread safe, observable collection base type for event dispatch
|
||||
/// </summary>
|
||||
/// <typeparam name="TV">Value type</typeparam>
|
||||
public abstract class MtObservableCollectionBase<TV> : INotifyPropertyChanged, INotifyCollectionChanged,
|
||||
ICollection, ICollection<TV>
|
||||
{
|
||||
private int _version;
|
||||
private readonly ThreadLocal<ThreadView> _threadViews;
|
||||
protected abstract ReaderWriterLockSlim Lock { get; }
|
||||
|
||||
protected MtObservableCollectionBase()
|
||||
{
|
||||
_version = 0;
|
||||
_threadViews = new ThreadLocal<ThreadView>(() => new ThreadView(this));
|
||||
_deferredSnapshot = new DeferredUpdateToken(this);
|
||||
_flushEventQueue = new Timer(FlushEventQueue);
|
||||
}
|
||||
|
||||
~MtObservableCollectionBase()
|
||||
{
|
||||
// normally we'd call Timer.Dispose() here but it's a managed handle, and the finalizer for the timerholder class does it
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should this observable collection actually dispatch events.
|
||||
/// </summary>
|
||||
public bool NotificationsEnabled { get; protected set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of this collection. Note: This call is only done when a read lock is acquired.
|
||||
/// </summary>
|
||||
/// <param name="old">Collection to clear and reuse, or null if none</param>
|
||||
/// <returns>The snapshot</returns>
|
||||
protected abstract List<TV> Snapshot(List<TV> old);
|
||||
|
||||
/// <summary>
|
||||
/// Marks all snapshots taken of this collection as dirty.
|
||||
/// </summary>
|
||||
protected void MarkSnapshotsDirty()
|
||||
{
|
||||
_version++;
|
||||
}
|
||||
|
||||
#region Event Wrappers
|
||||
|
||||
private readonly DeferredUpdateToken _deferredSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// Disposable that stops update signals and signals a full refresh when disposed.
|
||||
/// </summary>
|
||||
public IDisposable DeferredUpdate()
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
_deferredSnapshot.Enter();
|
||||
return _deferredSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private class DeferredUpdateToken : IDisposable
|
||||
{
|
||||
private readonly MtObservableCollectionBase<TV> _collection;
|
||||
private int _depth;
|
||||
|
||||
internal DeferredUpdateToken(MtObservableCollectionBase<TV> c)
|
||||
{
|
||||
_collection = c;
|
||||
}
|
||||
|
||||
internal void Enter()
|
||||
{
|
||||
if (Interlocked.Increment(ref _depth) == 1)
|
||||
{
|
||||
_collection.NotificationsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _depth) == 0)
|
||||
using (_collection.Lock.WriteUsing())
|
||||
{
|
||||
_collection.NotificationsEnabled = true;
|
||||
_collection.OnPropertyChanged("Count");
|
||||
_collection.OnPropertyChanged("Item[]");
|
||||
_collection.OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged(string propName)
|
||||
{
|
||||
if (!NotificationsEnabled)
|
||||
return;
|
||||
_propertyEventQueue.Enqueue(propName);
|
||||
_flushEventQueue?.Change(_eventRaiseDelay, -1);
|
||||
}
|
||||
|
||||
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (!NotificationsEnabled)
|
||||
return;
|
||||
_collectionEventQueue.Enqueue(e);
|
||||
// In half a second, flush the events
|
||||
_flushEventQueue?.Change(_eventRaiseDelay, -1);
|
||||
}
|
||||
|
||||
private readonly Timer _flushEventQueue;
|
||||
private const int _eventRaiseDelay = 50;
|
||||
|
||||
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
|
||||
new Queue<NotifyCollectionChangedEventArgs>();
|
||||
|
||||
private readonly Queue<string> _propertyEventQueue = new Queue<string>();
|
||||
|
||||
private void FlushEventQueue(object data)
|
||||
{
|
||||
// raise property events
|
||||
while (_propertyEventQueue.TryDequeue(out string prop))
|
||||
_propertyChangedEvent.Raise(this, new PropertyChangedEventArgs(prop));
|
||||
|
||||
// :/, but works better
|
||||
bool reset = _collectionEventQueue.Count > 0;
|
||||
if (reset)
|
||||
_collectionEventQueue.Clear();
|
||||
else
|
||||
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
|
||||
_collectionChangedEvent.Raise(this, e);
|
||||
|
||||
if (reset)
|
||||
_collectionChangedEvent.Raise(this,
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
private readonly MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler> _propertyChangedEvent
|
||||
=
|
||||
new MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
_propertyChangedEvent.Add(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
remove
|
||||
{
|
||||
_propertyChangedEvent.Remove(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>
|
||||
_collectionChangedEvent =
|
||||
new MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
_collectionChangedEvent.Add(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
remove
|
||||
{
|
||||
_collectionChangedEvent.Remove(value);
|
||||
OnPropertyChanged(nameof(IsObserved));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Is this collection observed by any listeners.
|
||||
/// </summary>
|
||||
public bool IsObserved => _collectionChangedEvent.IsObserved || _propertyChangedEvent.IsObserved;
|
||||
|
||||
#region Enumeration
|
||||
|
||||
/// <summary>
|
||||
/// Manages a snapshot to a collection and dispatches enumerators from that snapshot.
|
||||
/// </summary>
|
||||
private sealed class ThreadView
|
||||
{
|
||||
private readonly MtObservableCollectionBase<TV> _owner;
|
||||
private readonly WeakReference<List<TV>> _snapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MtObservableCollection{TC,TV}._version"/> of the <see cref="_snapshot"/>
|
||||
/// </summary>
|
||||
private int _snapshotVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Number of strong references to the value pointed to be <see cref="_snapshot"/>
|
||||
/// </summary>
|
||||
private int _snapshotRefCount;
|
||||
|
||||
internal ThreadView(MtObservableCollectionBase<TV> owner)
|
||||
{
|
||||
_owner = owner;
|
||||
_snapshot = new WeakReference<List<TV>>(null);
|
||||
_snapshotVersion = 0;
|
||||
_snapshotRefCount = 0;
|
||||
}
|
||||
|
||||
private List<TV> GetSnapshot()
|
||||
{
|
||||
// reading the version number + snapshots
|
||||
using (_owner.Lock.ReadUsing())
|
||||
{
|
||||
if (!_snapshot.TryGetTarget(out List<TV> currentSnapshot) || _snapshotVersion != _owner._version)
|
||||
{
|
||||
// Update the snapshot, using the old one if it isn't referenced.
|
||||
currentSnapshot = _owner.Snapshot(_snapshotRefCount == 0 ? currentSnapshot : null);
|
||||
_snapshotVersion = _owner._version;
|
||||
_snapshotRefCount = 0;
|
||||
_snapshot.SetTarget(currentSnapshot);
|
||||
}
|
||||
return currentSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Borrows a snapshot from a <see cref="ThreadView"/> and provides an enumerator.
|
||||
/// Once <see cref="Dispose"/> is called the read lock is released.
|
||||
/// </summary>
|
||||
internal sealed class Enumerator : IEnumerator<TV>
|
||||
{
|
||||
private readonly IEnumerator<TV> _backing;
|
||||
private readonly ThreadView _owner;
|
||||
private bool _disposed;
|
||||
|
||||
internal Enumerator(ThreadView owner)
|
||||
{
|
||||
_owner = owner;
|
||||
// Lock required since destructors run MT
|
||||
lock (_owner)
|
||||
{
|
||||
_owner._snapshotRefCount++;
|
||||
_backing = owner.GetSnapshot().GetEnumerator();
|
||||
}
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
~Enumerator()
|
||||
{
|
||||
// Lock required since destructors run MT
|
||||
if (!_disposed && _owner != null)
|
||||
lock (_owner)
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// safe deref so finalizer can clean up
|
||||
_backing?.Dispose();
|
||||
_owner._snapshotRefCount--;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
return _backing.MoveNext();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
_backing.Reset();
|
||||
}
|
||||
|
||||
public TV Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(Enumerator));
|
||||
return _backing.Current;
|
||||
}
|
||||
}
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<TV> GetEnumerator()
|
||||
{
|
||||
return new ThreadView.Enumerator(_threadViews.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract void CopyTo(Array array, int index);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract void Add(TV item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract void Clear();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool Contains(TV item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract void CopyTo(TV[] array, int arrayIndex);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool Remove(TV item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract int Count { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool IsReadOnly { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool ICollection.IsSynchronized => true;
|
||||
}
|
||||
}
|
@@ -1,163 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Multithread safe observable dictionary
|
||||
/// </summary>
|
||||
/// <typeparam name="TK">Key type</typeparam>
|
||||
/// <typeparam name="TV">Value type</typeparam>
|
||||
public class MtObservableDictionary<TK, TV> : MtObservableCollection<IDictionary<TK, TV>, KeyValuePair<TK, TV>>, IDictionary<TK, TV>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an empty observable dictionary
|
||||
/// </summary>
|
||||
public MtObservableDictionary() : base(new Dictionary<TK, TV>())
|
||||
{
|
||||
ObservableKeys = new ProxyCollection<TK>(this, Backing.Keys, (x) => x.Key);
|
||||
ObservableValues = new ProxyCollection<TV>(this, Backing.Values, (x) => x.Value);
|
||||
}
|
||||
|
||||
protected override List<KeyValuePair<TK, TV>> Snapshot(List<KeyValuePair<TK, TV>> old)
|
||||
{
|
||||
if (old == null)
|
||||
return new List<KeyValuePair<TK, TV>>(Backing);
|
||||
old.Clear();
|
||||
old.AddRange(Backing);
|
||||
return old;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ContainsKey(TK key)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return Backing.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(TK key, TV value)
|
||||
{
|
||||
Add(new KeyValuePair<TK, TV>(key, value));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(TK key)
|
||||
{
|
||||
return TryGetValue(key, out TV result) && Remove(new KeyValuePair<TK, TV>(key, result));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetValue(TK key, out TV value)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return Backing.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TV this[TK key]
|
||||
{
|
||||
get
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return Backing[key];
|
||||
}
|
||||
set
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
var oldKv = new KeyValuePair<TK, TV>(key, Backing[key]);
|
||||
var newKv = new KeyValuePair<TK, TV>(key, value);
|
||||
Backing[key] = value;
|
||||
MarkSnapshotsDirty();
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newKv, oldKv));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ICollection<TK> Keys => ObservableKeys;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ICollection<TV> Values => ObservableValues;
|
||||
|
||||
// TODO when we rewrite this to use a sorted dictionary.
|
||||
/// <inheritdoc cref="Keys"/>
|
||||
private ProxyCollection<TK> ObservableKeys { get; }
|
||||
|
||||
/// <inheritdoc cref="Keys"/>
|
||||
private ProxyCollection<TV> ObservableValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Proxy collection capable of raising notifications when the parent collection changes.
|
||||
/// </summary>
|
||||
/// <typeparam name="TP">Entry type</typeparam>
|
||||
public class ProxyCollection<TP> : ICollection<TP>
|
||||
{
|
||||
private readonly MtObservableDictionary<TK, TV> _owner;
|
||||
private readonly ICollection<TP> _backing;
|
||||
private readonly Func<KeyValuePair<TK, TV>, TP> _selector;
|
||||
|
||||
internal ProxyCollection(MtObservableDictionary<TK, TV> owner, ICollection<TP> backing, Func<KeyValuePair<TK, TV>, TP> selector)
|
||||
{
|
||||
_owner = owner;
|
||||
_backing = backing;
|
||||
_selector = selector;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<TP> GetEnumerator() => new TransformEnumerator<KeyValuePair<TK, TV>, TP>(_owner.GetEnumerator(), _selector);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(TP item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Contains(TP item)
|
||||
{
|
||||
using (_owner.Lock.ReadUsing())
|
||||
return _backing.Contains(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyTo(TP[] array, int arrayIndex)
|
||||
{
|
||||
using (_owner.Lock.ReadUsing())
|
||||
_backing.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(TP item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
using (_owner.Lock.ReadUsing())
|
||||
return _backing.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsReadOnly => _backing.IsReadOnly;
|
||||
}
|
||||
}
|
||||
}
|
369
Torch/Collections/MtObservableSortedDictionary.cs
Normal file
369
Torch/Collections/MtObservableSortedDictionary.cs
Normal file
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Multithread safe observable dictionary
|
||||
/// </summary>
|
||||
/// <typeparam name="TK">Key type</typeparam>
|
||||
/// <typeparam name="TV">Value type</typeparam>
|
||||
public class MtObservableSortedDictionary<TK, TV> :
|
||||
MtObservableCollectionBase<KeyValuePair<TK, TV>>, IDictionary<TK, TV>
|
||||
{
|
||||
private readonly IComparer<TK> _keyComparer;
|
||||
private readonly List<KeyValuePair<TK, TV>> _backing;
|
||||
private readonly KeysCollection _keyCollection;
|
||||
private readonly ValueCollection _valueCollection;
|
||||
|
||||
protected override ReaderWriterLockSlim Lock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty observable dictionary
|
||||
/// </summary>
|
||||
public MtObservableSortedDictionary(IComparer<TK> keyCompare = null)
|
||||
{
|
||||
_keyComparer = keyCompare ?? Comparer<TK>.Default;
|
||||
_backing = new List<KeyValuePair<TK, TV>>();
|
||||
_keyCollection = new KeysCollection(this);
|
||||
_valueCollection = new ValueCollection(this);
|
||||
Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override List<KeyValuePair<TK, TV>> Snapshot(List<KeyValuePair<TK, TV>> old)
|
||||
{
|
||||
if (old == null)
|
||||
return new List<KeyValuePair<TK, TV>>(_backing);
|
||||
old.Clear();
|
||||
old.AddRange(_backing);
|
||||
return old;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void CopyTo(Array array, int index)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
foreach (KeyValuePair<TK, TV> k in _backing)
|
||||
array.SetValue(k, index++);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return _backing.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Add(KeyValuePair<TK, TV> item)
|
||||
{
|
||||
Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Clear()
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
_backing.Clear();
|
||||
RaiseAllChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Contains(KeyValuePair<TK, TV> item)
|
||||
{
|
||||
return TryGetValue(item.Key, out var val) && EqualityComparer<TV>.Default.Equals(val, item.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void CopyTo(KeyValuePair<TK, TV>[] array, int arrayIndex)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
{
|
||||
_backing.CopyTo(array, arrayIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Remove(KeyValuePair<TK, TV> item)
|
||||
{
|
||||
return Remove(item.Key);
|
||||
}
|
||||
|
||||
private void RaiseAllChanged(NotifyCollectionChangedEventArgs evt)
|
||||
{
|
||||
MarkSnapshotsDirty();
|
||||
OnPropertyChanged(nameof(Count));
|
||||
OnPropertyChanged("Item[]");
|
||||
OnCollectionChanged(evt);
|
||||
_keyCollection.ParentChanged(evt);
|
||||
_valueCollection.ParentChanged(evt);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryGetBucket(TK key, out int res)
|
||||
{
|
||||
int min = 0, max = _backing.Count;
|
||||
while (min != max)
|
||||
{
|
||||
int mid = (min + max) / 2;
|
||||
if (_keyComparer.Compare(_backing[mid].Key, key) < 0)
|
||||
min = mid + 1;
|
||||
else
|
||||
max = mid;
|
||||
}
|
||||
res = min;
|
||||
return res < _backing.Count && _keyComparer.Compare(_backing[res].Key, key) == 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ContainsKey(TK key)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
return TryGetValue(key, out var _);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(TK key, TV value)
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
if (TryGetBucket(key, out var firstGtBucket))
|
||||
throw new ArgumentException($"Key {key} already exists", nameof(key));
|
||||
KeyValuePair<TK, TV> item = new KeyValuePair<TK, TV>(key, value);
|
||||
_backing.Insert(firstGtBucket, item);
|
||||
RaiseAllChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, firstGtBucket));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(TK key)
|
||||
{
|
||||
using (Lock.UpgradableReadUsing())
|
||||
{
|
||||
if (!TryGetBucket(key, out var bucket))
|
||||
return false;
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
KeyValuePair<TK, TV> old = _backing[bucket];
|
||||
_backing.RemoveAt(bucket);
|
||||
RaiseAllChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, bucket));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetValue(TK key, out TV value)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
{
|
||||
if (!TryGetBucket(key, out var bucket))
|
||||
{
|
||||
value = default(TV);
|
||||
return false;
|
||||
}
|
||||
value = _backing[bucket].Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TV this[TK key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetValue(key, out var result))
|
||||
return result;
|
||||
throw new KeyNotFoundException($"Key {key} not found");
|
||||
}
|
||||
set
|
||||
{
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
var item = new KeyValuePair<TK, TV>(key, value);
|
||||
if (TryGetBucket(key, out var firstGtBucket))
|
||||
{
|
||||
TV old = _backing[firstGtBucket].Value;
|
||||
_backing[firstGtBucket] = item;
|
||||
_valueCollection.ParentChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, old,
|
||||
value, firstGtBucket));
|
||||
}
|
||||
else
|
||||
{
|
||||
_backing.Insert(firstGtBucket, item);
|
||||
RaiseAllChanged(
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item,
|
||||
firstGtBucket));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ICollection<TK> IDictionary<TK, TV>.Keys => _keyCollection;
|
||||
ICollection<TV> IDictionary<TK, TV>.Values => _valueCollection;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TK, TV}.Keys"/>
|
||||
public MtObservableCollectionBase<TK> Keys => _keyCollection;
|
||||
|
||||
/// <inheritdoc cref="IDictionary{TK, TV}.Values"/>
|
||||
public MtObservableCollectionBase<TV> Values => _valueCollection;
|
||||
|
||||
private abstract class ProxyCollection<TT> : MtObservableCollectionBase<TT>
|
||||
{
|
||||
protected readonly MtObservableSortedDictionary<TK, TV> Owner;
|
||||
private readonly Func<KeyValuePair<TK, TV>, TT> _selector;
|
||||
|
||||
protected override ReaderWriterLockSlim Lock => Owner.Lock;
|
||||
|
||||
protected ProxyCollection(MtObservableSortedDictionary<TK, TV> owner,
|
||||
Func<KeyValuePair<TK, TV>, TT> selector)
|
||||
{
|
||||
Owner = owner;
|
||||
_selector = selector;
|
||||
}
|
||||
|
||||
|
||||
protected override List<TT> Snapshot(List<TT> old)
|
||||
{
|
||||
if (old == null)
|
||||
old = new List<TT>(Owner._backing.Count);
|
||||
else
|
||||
old.Clear();
|
||||
foreach (KeyValuePair<TK, TV> kv in Owner._backing)
|
||||
old.Add(_selector(kv));
|
||||
return old;
|
||||
}
|
||||
|
||||
public override void CopyTo(Array array, int index)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
{
|
||||
foreach (KeyValuePair<TK, TV> entry in Owner._backing)
|
||||
array.SetValue(_selector(entry), index++);
|
||||
}
|
||||
}
|
||||
|
||||
public override int Count => Owner.Count;
|
||||
public override bool IsReadOnly => Owner.IsReadOnly;
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
Owner.Clear();
|
||||
}
|
||||
|
||||
public override void CopyTo(TT[] array, int arrayIndex)
|
||||
{
|
||||
using (Lock.ReadUsing())
|
||||
{
|
||||
foreach (KeyValuePair<TK, TV> entry in Owner._backing)
|
||||
array[arrayIndex++] = _selector(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private IList TransformList(IEnumerable list)
|
||||
{
|
||||
if (list == null) return null;
|
||||
ArrayList res = new ArrayList();
|
||||
foreach (object k in list)
|
||||
res.Add(_selector((KeyValuePair<TK, TV>) k));
|
||||
return res;
|
||||
}
|
||||
|
||||
public void ParentChanged(NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
OnPropertyChanged(nameof(Count));
|
||||
OnPropertyChanged("Item[]");
|
||||
if (args.Action == NotifyCollectionChangedAction.Reset)
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
else if (args.OldItems == null && args.OldStartingIndex == -1)
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(args.Action, TransformList(args.NewItems),
|
||||
args.NewStartingIndex));
|
||||
else if (args.NewItems == null && args.NewStartingIndex == -1)
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(args.Action, TransformList(args.OldItems),
|
||||
args.OldStartingIndex));
|
||||
else if (ReferenceEquals(args.NewItems, args.OldItems))
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(args.Action, TransformList(args.NewItems),
|
||||
args.NewStartingIndex, args.OldStartingIndex));
|
||||
else if (args.NewStartingIndex == args.OldStartingIndex && args.NewItems != null &&
|
||||
args.OldItems != null)
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(args.Action, TransformList(args.NewItems),
|
||||
TransformList(args.OldItems),
|
||||
args.NewStartingIndex));
|
||||
else
|
||||
Debugger.Break();
|
||||
}
|
||||
}
|
||||
|
||||
private class KeysCollection : ProxyCollection<TK>
|
||||
{
|
||||
public KeysCollection(MtObservableSortedDictionary<TK, TV> owner) : base(owner, x => x.Key)
|
||||
{
|
||||
}
|
||||
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
public override void Add(TK item) => Owner.Add(item, default(TV));
|
||||
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
public override bool Contains(TK item) => Owner.ContainsKey(item);
|
||||
|
||||
// ReSharper disable once AssignNullToNotNullAttribute
|
||||
public override bool Remove(TK item) => Owner.Remove(item);
|
||||
}
|
||||
|
||||
private class ValueCollection : ProxyCollection<TV>
|
||||
{
|
||||
public ValueCollection(MtObservableSortedDictionary<TK, TV> owner) : base(owner, x => x.Value)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Add(TV item)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool Contains(TV item)
|
||||
{
|
||||
EqualityComparer<TV> cmp = EqualityComparer<TV>.Default;
|
||||
using (Lock.ReadUsing())
|
||||
foreach (KeyValuePair<TK, TV> kv in Owner._backing)
|
||||
if (cmp.Equals(kv.Value, item))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Remove(TV item)
|
||||
{
|
||||
EqualityComparer<TV> cmp = EqualityComparer<TV>.Default;
|
||||
var hasKey = false;
|
||||
TK key = default(TK);
|
||||
using (Lock.ReadUsing())
|
||||
foreach (KeyValuePair<TK, TV> kv in Owner._backing)
|
||||
if (cmp.Equals(kv.Value, item))
|
||||
{
|
||||
hasKey = true;
|
||||
key = kv.Key;
|
||||
break;
|
||||
}
|
||||
return hasKey && Owner.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -48,7 +48,7 @@ namespace Torch.Managers
|
||||
/// <inheritdoc />
|
||||
public event Action<IPlayer> PlayerLeft;
|
||||
|
||||
public MtObservableDictionary<ulong, PlayerViewModel> Players { get; } = new MtObservableDictionary<ulong, PlayerViewModel>();
|
||||
public MtObservableSortedDictionary<ulong, PlayerViewModel> Players { get; } = new MtObservableSortedDictionary<ulong, PlayerViewModel>();
|
||||
|
||||
#pragma warning disable 649
|
||||
[ReflectedGetter(Name = "m_players")]
|
||||
|
@@ -28,7 +28,7 @@ namespace Torch.Managers
|
||||
private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
|
||||
private const string MANIFEST_NAME = "manifest.xml";
|
||||
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
|
||||
private readonly MtObservableDictionary<Guid, ITorchPlugin> _plugins = new MtObservableDictionary<Guid, ITorchPlugin>();
|
||||
private readonly MtObservableSortedDictionary<Guid, ITorchPlugin> _plugins = new MtObservableSortedDictionary<Guid, ITorchPlugin>();
|
||||
#pragma warning disable 649
|
||||
[Dependency]
|
||||
private ITorchSessionManager _sessionManager;
|
||||
|
@@ -155,7 +155,8 @@
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Collections\MtObservableCollection.cs" />
|
||||
<Compile Include="Collections\MtObservableDictionary.cs" />
|
||||
<Compile Include="Collections\MtObservableCollectionBase.cs" />
|
||||
<Compile Include="Collections\MtObservableSortedDictionary.cs" />
|
||||
<Compile Include="Collections\MtObservableEvent.cs" />
|
||||
<Compile Include="Collections\MtObservableList.cs" />
|
||||
<Compile Include="Collections\TransformComparer.cs" />
|
||||
|
@@ -232,8 +232,11 @@ namespace Torch
|
||||
public void InvokeBlocking(Action action, int timeoutMs = -1, [CallerMemberName] string caller = "")
|
||||
{
|
||||
// ReSharper disable once ExplicitCallerInfoArgument
|
||||
if (!InvokeAsync(action, caller).Wait(timeoutMs))
|
||||
Task task = InvokeAsync(action, caller);
|
||||
if (!task.Wait(timeoutMs))
|
||||
throw new TimeoutException("The game action timed out");
|
||||
if (task.IsFaulted && task.Exception != null)
|
||||
throw task.Exception;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -312,9 +315,9 @@ namespace Torch
|
||||
return ctx.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Torch Init/Destroy
|
||||
#region Torch Init/Destroy
|
||||
|
||||
protected abstract uint SteamAppId { get; }
|
||||
protected abstract string SteamAppName { get; }
|
||||
@@ -380,7 +383,7 @@ namespace Torch
|
||||
Game = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
protected VRageGame Game { get; private set; }
|
||||
|
||||
|
Reference in New Issue
Block a user