diff --git a/EDSEditor.sln b/EDSEditor.sln index 5b28768c..00abe8c3 100644 --- a/EDSEditor.sln +++ b/EDSEditor.sln @@ -14,6 +14,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EDSSharp", "EDSSharp\EDSSharp.csproj", "{8B7A7545-6257-44BF-8868-F429E1B72C77}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EDSEditorGUI2", "EDSEditorGUI2\EDSEditorGUI2.csproj", "{F175A47B-8BB8-480F-8D31-AF802086B8B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUITests", "GUITests\GUITests.csproj", "{9B9B5461-1864-484D-A49D-D39422DA16E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +40,14 @@ Global {8B7A7545-6257-44BF-8868-F429E1B72C77}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B7A7545-6257-44BF-8868-F429E1B72C77}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B7A7545-6257-44BF-8868-F429E1B72C77}.Release|Any CPU.Build.0 = Release|Any CPU + {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F175A47B-8BB8-480F-8D31-AF802086B8B4}.Release|Any CPU.Build.0 = Release|Any CPU + {9B9B5461-1864-484D-A49D-D39422DA16E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B9B5461-1864-484D-A49D-D39422DA16E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B9B5461-1864-484D-A49D-D39422DA16E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B9B5461-1864-484D-A49D-D39422DA16E0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EDSEditorGUI2/.gitignore b/EDSEditorGUI2/.gitignore new file mode 100644 index 00000000..fe7ab09c --- /dev/null +++ b/EDSEditorGUI2/.gitignore @@ -0,0 +1,2 @@ +/obj/* +/bin/* diff --git a/EDSEditorGUI2/App.axaml b/EDSEditorGUI2/App.axaml new file mode 100644 index 00000000..c1793b31 --- /dev/null +++ b/EDSEditorGUI2/App.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EDSEditorGUI2/App.axaml.cs b/EDSEditorGUI2/App.axaml.cs new file mode 100644 index 00000000..07140117 --- /dev/null +++ b/EDSEditorGUI2/App.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using EDSEditorGUI2.ViewModels; +using EDSEditorGUI2.Views; + +namespace EDSEditorGUI2; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + BindingPlugins.DataValidators.RemoveAt(0); + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/Assets/avalonia-logo.ico b/EDSEditorGUI2/Assets/avalonia-logo.ico new file mode 100644 index 00000000..da8d49ff Binary files /dev/null and b/EDSEditorGUI2/Assets/avalonia-logo.ico differ diff --git a/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs b/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs new file mode 100644 index 00000000..5a5b8d47 --- /dev/null +++ b/EDSEditorGUI2/Converter/NewIndexMultiConvert.cs @@ -0,0 +1,46 @@ +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Media.Immutable; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace EDSEditorGUI2.Converter; + +public class NewIndexRequest(int index, string name, LibCanOpen.OdObject.Types.ObjectType type) +{ + public int Index { get; } = index; + public string Name { get; } = name; + public LibCanOpen.OdObject.Types.ObjectType Type { get; } = type; +} + +public sealed class NewIndexMultiConvert : IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + // Ensure all bindings are provided and attached to correct target type + if (values?.Count != 3 || !targetType.IsAssignableFrom(typeof(ImmutableSolidColorBrush))) + throw new NotSupportedException(); + + if (values[0] is not string rawindex || + values[1] is not string name || + values[2] is not int typeIndex) + return BindingOperations.DoNothing; + + int index = int.Parse(rawindex, NumberStyles.HexNumber); + + var typeValues = Enum.GetNames(typeof(LibCanOpen.OdObject.Types.ObjectType)).Skip(1).ToArray(); + bool parseOk = Enum.TryParse(typeValues[typeIndex], out LibCanOpen.OdObject.Types.ObjectType type); + + if (parseOk) + { + var indexRequest = new NewIndexRequest(index, name, type); + return indexRequest; + } + else + { + return BindingOperations.DoNothing; + } + } +} diff --git a/EDSEditorGUI2/EDSEditorGUI2.csproj b/EDSEditorGUI2/EDSEditorGUI2.csproj new file mode 100644 index 00000000..ab887d37 --- /dev/null +++ b/EDSEditorGUI2/EDSEditorGUI2.csproj @@ -0,0 +1,30 @@ + + + WinExe + net8.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/EDSEditorGUI2/Extensions.cs b/EDSEditorGUI2/Extensions.cs new file mode 100644 index 00000000..2ccc548a --- /dev/null +++ b/EDSEditorGUI2/Extensions.cs @@ -0,0 +1,15 @@ +using System; + +namespace EDSEditorGUI2 +{ + public static class StringExtensions + { + /// + /// Convert different types of hex/dec string to integer + /// + public static UInt16 ToInteger(this String val) + { + return (UInt16)Convert.ToInt32(val, 16); + } + } +} diff --git a/EDSEditorGUI2/Icons.axaml b/EDSEditorGUI2/Icons.axaml new file mode 100644 index 00000000..ef341445 --- /dev/null +++ b/EDSEditorGUI2/Icons.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + M3.7547787,12.4995322 L20.2466903,12.4995322 C20.6609039,12.4995322 20.9966903,12.1637458 20.9966903,11.7495322 C20.9966903,11.3353187 20.6609039,10.9995322 20.2466903,10.9995322 L3.7547787,10.9995322 C3.34056514,10.9995322 3.0047787,11.3353187 3.0047787,11.7495322 C3.0047787,12.1637458 3.34056514,12.4995322 3.7547787,12.4995322 Z + M14.5,13 L14.5,3.75378577 C14.5,3.33978577 14.164,3.00378577 13.75,3.00378577 C13.336,3.00378577 13,3.33978577 13,3.75378577 L13,13 L3.75387573,13 C3.33987573,13 3.00387573,13.336 3.00387573,13.75 C3.00387573,14.164 3.33987573,14.5 3.75387573,14.5 L13,14.5 L13,23.7523651 C13,24.1663651 13.336,24.5023651 13.75,24.5023651 C14.164,24.5023651 14.5,24.1663651 14.5,23.7523651 L14.5,14.5 L23.7498262,14.5030754 C24.1638262,14.5030754 24.4998262,14.1670754 24.4998262,13.7530754 C24.4998262,13.3390754 24.1638262,13.0030754 23.7498262,13.0030754 L14.5,13 Z + M3 5.75C3 4.23122 4.23122 3 5.75 3H15.7145C16.5764 3 17.4031 3.34241 18.0126 3.9519L20.0481 5.98744C20.6576 6.59693 21 7.42358 21 8.28553V18.25C21 19.7688 19.7688 21 18.25 21H5.75C4.23122 21 3 19.7688 3 18.25V5.75ZM5.75 4.5C5.05964 4.5 4.5 5.05964 4.5 5.75V18.25C4.5 18.9404 5.05964 19.5 5.75 19.5H6V14.25C6 13.0074 7.00736 12 8.25 12H15.75C16.9926 12 18 13.0074 18 14.25V19.5H18.25C18.9404 19.5 19.5 18.9404 19.5 18.25V8.28553C19.5 7.8214 19.3156 7.37629 18.9874 7.0481L16.9519 5.01256C16.6918 4.75246 16.3582 4.58269 16 4.52344V7.25C16 8.49264 14.9926 9.5 13.75 9.5H9.25C8.00736 9.5 7 8.49264 7 7.25V4.5H5.75ZM16.5 19.5V14.25C16.5 13.8358 16.1642 13.5 15.75 13.5H8.25C7.83579 13.5 7.5 13.8358 7.5 14.25V19.5H16.5ZM8.5 4.5V7.25C8.5 7.66421 8.83579 8 9.25 8H13.75C14.1642 8 14.5 7.66421 14.5 7.25V4.5H8.5Z + + + diff --git a/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs b/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs new file mode 100644 index 00000000..9fff16ce --- /dev/null +++ b/EDSEditorGUI2/Mapper/ProtobufferViewModelMapper.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using Google.Protobuf.WellKnownTypes; +using LibCanOpen; +using System; + +namespace EDSEditorGUI2.Mapper +{ + public partial class ProtobufferViewModelMapper + { + public static ViewModels.Device MapFromProtobuffer(CanOpenDevice source) + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap().ConvertUsing(ts => ts.ToDateTime()); + cfg.CreateMap() + .ForMember(dest => dest.FileVersion, opt => opt.MapFrom(src => src.FileVersion)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.CreatedBy, opt => opt.MapFrom(src => src.CreatedBy)) + .ForMember(dest => dest.ModificationTime, opt => opt.MapFrom(src => src.ModificationTime)) + .ForMember(dest => dest.ModifiedBy, opt => opt.MapFrom(src => src.ModifiedBy)); + cfg.CreateMap() + .ForMember(dest => dest.FileInfo, opt => opt.MapFrom(src => src.FileInfo)) + .ForMember(dest => dest.DeviceInfo, opt => opt.MapFrom(src => src.DeviceInfo)) + .ForMember(dest => dest.DeviceCommissioning, opt => opt.MapFrom(src => src.DeviceCommissioning)) + .ForPath(dest => dest.Objects.Data, opt => opt.MapFrom(src => src.Objects)); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + + }); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + var result = mapper.Map(source); + return result; + } + + public static CanOpenDevice MapToProtobuffer(ViewModels.Device source) + { + var config = new MapperConfiguration(cfg => + { + //TODO + }); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + var result = mapper.Map(source); + return result; + } + } +} diff --git a/EDSEditorGUI2/Program.cs b/EDSEditorGUI2/Program.cs new file mode 100644 index 00000000..c22186a5 --- /dev/null +++ b/EDSEditorGUI2/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace EDSEditorGUI2; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/EDSEditorGUI2/ViewLocator.cs b/EDSEditorGUI2/ViewLocator.cs new file mode 100644 index 00000000..c52fcee4 --- /dev/null +++ b/EDSEditorGUI2/ViewLocator.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using EDSEditorGUI2.ViewModels; +using System; + +namespace EDSEditorGUI2; + +public class ViewLocator : IDataTemplate +{ + + public Control? Build(object? data) + { + if (data is null) + return null; + + var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + var control = (Control)Activator.CreateInstance(type)!; + control.DataContext = data; + return control; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/EDSEditorGUI2/ViewModels/Device.cs b/EDSEditorGUI2/ViewModels/Device.cs new file mode 100644 index 00000000..82868a44 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/Device.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace EDSEditorGUI2.ViewModels +{ + public partial class Device : ObservableObject + { + public Device() + { + } + + [ObservableProperty] + private FileInfo _fileInfo = new(); + + [ObservableProperty] + private DeviceInfo _deviceInfo = new(); + + [ObservableProperty] + private DeviceCommissioning _deviceCommissioning = new(); + + [ObservableProperty] + private DeviceOD _objects = new(); + + public void OnClickCommand() + { + // do something + } + } +} + diff --git a/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs b/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs new file mode 100644 index 00000000..aee74b5d --- /dev/null +++ b/EDSEditorGUI2/ViewModels/DeviceCommissioning.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace EDSEditorGUI2.ViewModels; + +public partial class DeviceCommissioning : ObservableObject +{ + [ObservableProperty] + private UInt32 _nodeId; + + [ObservableProperty] + private string _nodeName = string.Empty; + + [ObservableProperty] + private UInt32 _baudrate; +} diff --git a/EDSEditorGUI2/ViewModels/DeviceInfo.cs b/EDSEditorGUI2/ViewModels/DeviceInfo.cs new file mode 100644 index 00000000..c9111e83 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/DeviceInfo.cs @@ -0,0 +1,45 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace EDSEditorGUI2.ViewModels; + +public partial class DeviceInfo : ObservableObject +{ + [ObservableProperty] + string _vendorName = string.Empty; + + [ObservableProperty] + string _productName = string.Empty; + + [ObservableProperty] + bool _baudRate10; + + [ObservableProperty] + bool _baudRate20; + + [ObservableProperty] + bool _baudRate50; + + [ObservableProperty] + bool _baudRate125; + + [ObservableProperty] + bool _baudRate250; + + [ObservableProperty] + bool _baudRate500; + + [ObservableProperty] + bool _baudRate800; + + [ObservableProperty] + bool _baudRate1000; + + [ObservableProperty] + bool _baudRateAuto; + + [ObservableProperty] + bool _lssSlave; + + [ObservableProperty] + bool _lssMaster; +} diff --git a/EDSEditorGUI2/ViewModels/DeviceOD.cs b/EDSEditorGUI2/ViewModels/DeviceOD.cs new file mode 100644 index 00000000..03b2128b --- /dev/null +++ b/EDSEditorGUI2/ViewModels/DeviceOD.cs @@ -0,0 +1,91 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace EDSEditorGUI2.ViewModels; +public partial class DeviceOD : ObservableObject +{ + public ObservableCollection> Data { get; } = []; + + [ObservableProperty] + KeyValuePair _selectedObject; + + [ObservableProperty] + KeyValuePair _selectedSubObject; + + [ObservableProperty] + ObservableCollection> _selectedSubObjects = []; + + public DeviceOD() + { + } + + private static int IndexStringToInt(string str) + { + if (str.StartsWith("0x")) + { + var hex = str[2..]; + return Convert.ToUInt16(hex, 16); + } + else + { + return Convert.ToUInt16(str); + } + } + + public void AddIndex(int index, string name, LibCanOpen.OdObject.Types.ObjectType type) + { + var strIndex = index.ToString("X4"); + var newObj = new OdObject + { + Name = name, + Type = type + }; + + // create OD entry + if (type == LibCanOpen.OdObject.Types.ObjectType.Var) + { + var newSub = new OdSubObject() + { + Name = name, + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + }; + newObj.SubObjects.Add(new KeyValuePair("0x0", newSub)); + } + else + { + var CountSub = new OdSubObject() + { + Name = "Highest sub-index supported", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned8, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Ro, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0x01" + }; + var Sub1 = new OdSubObject() + { + Name = "Sub Object 1", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + }; + + newObj.SubObjects.Add(new KeyValuePair("0x0", CountSub)); + newObj.SubObjects.Add(new KeyValuePair("0x1", Sub1)); + } + Data.Add(new KeyValuePair(strIndex, newObj)); + } + + public void RemoveIndex(object sender) + { + + } +} diff --git a/EDSEditorGUI2/ViewModels/FileInfo.cs b/EDSEditorGUI2/ViewModels/FileInfo.cs new file mode 100644 index 00000000..7d2f5596 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/FileInfo.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace EDSEditorGUI2.ViewModels; + +public partial class FileInfo : ObservableObject +{ + [ObservableProperty] + private string _fileVersion = string.Empty; + + [ObservableProperty] + private string _description = string.Empty; + + [ObservableProperty] + private DateTime _creationTime; + + [ObservableProperty] + private string _createdBy = string.Empty; + + [ObservableProperty] + private DateTime _modificationTime; + + [ObservableProperty] + private string _modifiedBy = string.Empty; +} diff --git a/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs b/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..9d0dfdf9 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,45 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using EDSEditorGUI2.Mapper; +using System.Collections.ObjectModel; + +namespace EDSEditorGUI2.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + int Counter = 0; + public void AddNewDevice(object sender) + { + var device = new LibCanOpen.CanOpenDevice + { + DeviceInfo = new() + { + ProductName = "New Product" + Counter.ToString() + }, + }; + + Counter++; + + //string dir = Environment.OSVersion.Platform == PlatformID.Win32NT ? "\\" : "/"; + //eds.projectFilename = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + dir + "project"; + + //DeviceView device = new DeviceView(eds, network); + //device.UpdateODViewForEDS += Device_UpdateODViewForEDS; + + //eds.OnDataDirty += Eds_onDataDirty; + + //device.Dock = DockStyle.Fill; + //device.dispatch_updateOD(); + + var DeviceView = ProtobufferViewModelMapper.MapFromProtobuffer(device); + Network.Add(DeviceView); + } +#pragma warning disable CA1822 // Mark members as static + public string Greeting => "Welcome to Avalonia!"; +#pragma warning restore CA1822 // Mark members as static + public ObservableCollection Network { get; set; } = []; + + [ObservableProperty] + public Device? selectedDevice; + + +} diff --git a/EDSEditorGUI2/ViewModels/OdObject.cs b/EDSEditorGUI2/ViewModels/OdObject.cs new file mode 100644 index 00000000..02e90e64 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/OdObject.cs @@ -0,0 +1,155 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using libEDSsharp; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace EDSEditorGUI2.ViewModels +{ + public partial class OdObject : ObservableObject + { + [ObservableProperty] + private bool _disabled; + + [ObservableProperty] + string _name = string.Empty; + + [ObservableProperty] + string _alias = string.Empty; + + [ObservableProperty] + string _description = string.Empty; + + [ObservableProperty] + LibCanOpen.OdObject.Types.ObjectType _type; + + [ObservableProperty] + string _countLabel = string.Empty; + + [ObservableProperty] + string _storageGroup = string.Empty; + + [ObservableProperty] + bool flagsPDO; + + [ObservableProperty] + ObservableCollection> _subObjects = []; + + /// + /// very based on ODentry.AddSubEntry + /// + public OdSubObject? AddSubEntry(KeyValuePair selected) + { + if (Type == LibCanOpen.OdObject.Types.ObjectType.Var) + return null; + + OdSubObject newOd; + + //Do we need the type check?? + if ((SubObjects.Count == 0) && ((Type == LibCanOpen.OdObject.Types.ObjectType.Array) || (Type == LibCanOpen.OdObject.Types.ObjectType.Record))) + { + SubObjects.Add(new KeyValuePair("0", new OdSubObject + { + Name = "Highest sub-index supported", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned8, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Ro, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0x01" + })); + } + + var lastSubOd = SubObjects.Last(); + UInt16 maxSubIndex = 1; + UInt16 lastSubIndex = 1; + + // create new or clone existing sub od + if (lastSubOd.Value == null || lastSubOd.Key.ToInteger() < 1) + { + newOd = new OdSubObject + { + Name = "item", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32 + }; + } + else + { + newOd = new OdSubObject + { + //TODO: make a clone function with reflection to keep it up-to-date + Name = selected.Value.Name, + Alias = selected.Value.Alias, + Type = selected.Value.Type, + Sdo = selected.Value.Sdo, + Pdo = selected.Value.Pdo, + Srdo = selected.Value.Srdo, + DefaultValue = selected.Value.DefaultValue, + ActualValue = selected.Value.ActualValue, + LowLimit = selected.Value.LowLimit, + HighLimit = selected.Value.HighLimit, + StringLengthMin = selected.Value.StringLengthMin, + }; + } + + // insert new sub od + ObservableCollection> newSubObjects = []; + UInt16 newSubIndex = 0; + foreach (var sub in SubObjects) + { + var subOd = sub.Value; + if (sub.Key.ToInteger() > newSubIndex) + newSubIndex = sub.Key.ToInteger(); + + newSubObjects.Add(new KeyValuePair((newSubIndex++).ToHexString(), subOd)); + + if (selected.Value == subOd) + newSubObjects.Add(new KeyValuePair((newSubIndex++).ToHexString(), newOd)); + } + + SubObjects = newSubObjects; + + // Write maxSubIndex to first sub index + if (maxSubIndex > 0 && maxSubIndex == lastSubIndex && SubObjects.Count > 0) + { + SubObjects[0].Value.DefaultValue = string.Format("0x{0:X2}", newSubIndex - 1); + } + + return newOd; + } + /// + /// Remove current sub entry + /// + /// Keyvalue pair of the subindex to be removed + /// Renumber subentries + /// true on successfull removal + public bool RemoveSubEntry(KeyValuePair subObjectToRemove, bool renumber) + { + if (Type == LibCanOpen.OdObject.Types.ObjectType.Array || Type == LibCanOpen.OdObject.Types.ObjectType.Record) + { + UInt16 maxSubIndex = SubObjects[0].Value.DefaultValue.ToInteger(); + UInt16 lastSubIndex = SubObjects.Last().Key.ToInteger(); + + SubObjects.Remove(subObjectToRemove); + + if (renumber) + { + ObservableCollection> newSubObjects = []; + UInt16 subIndex = 0; + foreach (var subOd in SubObjects) + newSubObjects.Add(new KeyValuePair((subIndex++).ToHexString(), subOd.Value)); + + SubObjects = newSubObjects; + } + + // Write maxSubIndex to first sub index + if (maxSubIndex > 0 && maxSubIndex == lastSubIndex && SubObjects.Count > 0) + { + SubObjects[0].Value.DefaultValue = string.Format("0x{0:X2}", SubObjects.Last().Key.ToInteger()); + } + return true; + } + return false; + } + } +} diff --git a/EDSEditorGUI2/ViewModels/OdSubObject.cs b/EDSEditorGUI2/ViewModels/OdSubObject.cs new file mode 100644 index 00000000..99d0b946 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/OdSubObject.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using static LibCanOpen.OdSubObject.Types; + +namespace EDSEditorGUI2.ViewModels; + +public partial class OdSubObject : ObservableObject +{ + [ObservableProperty] + private string _name = string.Empty; + + [ObservableProperty] + private string _alias = string.Empty; + + [ObservableProperty] + private DataType _type; + + [ObservableProperty] + private AccessSDO _sdo; + + [ObservableProperty] + private AccessPDO _pdo; + + [ObservableProperty] + private AccessSRDO _srdo; + + [ObservableProperty] + private string _defaultValue = string.Empty; + + [ObservableProperty] + private string _actualValue = string.Empty; + + [ObservableProperty] + private string _lowLimit = string.Empty; + + [ObservableProperty] + string _highLimit = string.Empty; + + [ObservableProperty] + private uint _stringLengthMin; +} diff --git a/EDSEditorGUI2/ViewModels/ViewModelBase.cs b/EDSEditorGUI2/ViewModels/ViewModelBase.cs new file mode 100644 index 00000000..a4565d46 --- /dev/null +++ b/EDSEditorGUI2/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace EDSEditorGUI2.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/EDSEditorGUI2/Views/DeviceInfoView.axaml b/EDSEditorGUI2/Views/DeviceInfoView.axaml new file mode 100644 index 00000000..a624698e --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceInfoView.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + LSS Slave + Master + + + Baudrates + 10 kbit/s. + 20 kbit/s. + 50 kbit/s. + 125 kbit/s. + 250 kbit/s. + 500 kbit/s. + 800 kbit/s. + 1000 kbit/s. + auto + + + + + diff --git a/EDSEditorGUI2/Views/DeviceInfoView.axaml.cs b/EDSEditorGUI2/Views/DeviceInfoView.axaml.cs new file mode 100644 index 00000000..fbf6126a --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceInfoView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace EDSEditorGUI2.Views; + +public partial class DeviceInfoView : UserControl +{ + public DeviceInfoView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/Views/DeviceODView.axaml b/EDSEditorGUI2/Views/DeviceODView.axaml new file mode 100644 index 00000000..0b5453f1 --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceODView.axaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Autosave + + + + + + diff --git a/EDSEditorGUI2/Views/DeviceODView.axaml.cs b/EDSEditorGUI2/Views/DeviceODView.axaml.cs new file mode 100644 index 00000000..74cc6e93 --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceODView.axaml.cs @@ -0,0 +1,113 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using LibCanOpen; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace EDSEditorGUI2.Views; + +public partial class DeviceODView : UserControl +{ + private List _odViews = []; + public DeviceODView() + { + InitializeComponent(); + ODView_Com.grid.SelectionChanged += IndexGridSelectionChanged; + ODView_Manufacture.grid.SelectionChanged += IndexGridSelectionChanged; + ODView_Device.grid.SelectionChanged += IndexGridSelectionChanged; + + subindexGrid.SelectionChanged += subindexGridSelectionChanged; + + _odViews.Add(ODView_Com.grid); + _odViews.Add(ODView_Manufacture.grid); + _odViews.Add(ODView_Device.grid); + + foreach (var v in Enum.GetNames(typeof(OdSubObject.Types.DataType))) + { + combo_datatype.Items.Add(v); + } + + foreach (var v in Enum.GetNames(typeof(OdSubObject.Types.AccessSDO))) + { + combo_sdo.Items.Add(v); + } + + foreach (var v in Enum.GetNames(typeof(OdSubObject.Types.AccessPDO))) + { + combo_pdo.Items.Add(v); + } + + foreach (var v in Enum.GetNames(typeof(OdSubObject.Types.AccessSRDO))) + { + combo_srdo.Items.Add(v); + } + } + + private void IndexGridSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid s && DataContext is ViewModels.DeviceOD dc) + { + if (s.SelectedItem is KeyValuePair selected) + { + dc.SelectedObject = selected; + foreach (var dg in _odViews) + { + if (dg != s) + { + dg.SelectedItem = null; + subindexGrid.SelectedItem = null; + } + } + } + } + } + private void subindexGridSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid s && DataContext is ViewModels.DeviceOD dc) + { + if (s.SelectedItem is KeyValuePair selected) + { + dc.SelectedSubObject = selected; + dc.SelectedSubObjects.Clear(); + foreach (var row in s.SelectedItems) + { + if (row is KeyValuePair subObj) + { + dc.SelectedSubObjects.Add(subObj); + } + } + } + } + } + private void ContextMenuSubObjectAddClick(object? sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.DeviceOD dc) + { + var selectedObj = dc.SelectedObject.Value; + ObservableCollection> selection = []; + + foreach (var row in dc.SelectedSubObjects) + { + selectedObj.AddSubEntry(row); + } + } + } + private void ContextMenuSubObjectRemoveClick(object? sender, RoutedEventArgs e) + { + bool renumber = sender == contextMenu_subObject_removeSubItemToolStripMenuItem; + + if (DataContext is ViewModels.DeviceOD dc) + { + var selectedObject = dc.SelectedObject.Value; + + //Clone the list because we cant modify the list we iterate on + var selectedObj = dc.SelectedSubObjects.ToList(); + foreach (var item in selectedObj) + { + selectedObject.RemoveSubEntry(item, renumber); + } + } + } +} diff --git a/EDSEditorGUI2/Views/DevicePDOView.axaml b/EDSEditorGUI2/Views/DevicePDOView.axaml new file mode 100644 index 00000000..0fe71fc2 --- /dev/null +++ b/EDSEditorGUI2/Views/DevicePDOView.axaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Invalid + + + + + + + + Zoom: + + + diff --git a/EDSEditorGUI2/Views/DevicePDOView.axaml.cs b/EDSEditorGUI2/Views/DevicePDOView.axaml.cs new file mode 100644 index 00000000..b7917baf --- /dev/null +++ b/EDSEditorGUI2/Views/DevicePDOView.axaml.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Media; +using System.Collections.Generic; + +namespace EDSEditorGUI2.Views; + +public partial class DevicePDOView : UserControl +{ + private List _bitColumns = []; + public DevicePDOView() + { + InitializeComponent(); + + CreateMappingBitsAndBytesIndication(); + Zoom.Value = 100; + } + + void CreateMappingBitsAndBytesIndication() + { + //Bits + for (int i = 0; i < 64; i++) + { + var indication = new TextBlock + { + Text = i.ToString(), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap, + }; + if ((i % 8) == 0) + { + indication.Foreground = Brushes.Red; + } + AddToMappingGrid(indication, 0, 3 + i); + + var newColumn = new ColumnDefinition(new GridLength(10 * 1.0)); + _bitColumns.Add(newColumn); + + MappingGrid.ColumnDefinitions.Add(newColumn); + } + //Bytes + for (int i = 0; i < 8; i++) + { + var indication = new TextBlock + { + Text = $"Byte {i}", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + + TextWrapping = TextWrapping.Wrap, + }; + AddToMappingGrid(indication, 1, 3 + (i*8), 8); + } + } + void AddToMappingGrid(Control element, int row,int column, int columnspam = 1) + { + Grid.SetRow(element, row); + Grid.SetColumn(element, column); + Grid.SetColumnSpan(element, columnspam); + MappingGrid.Children.Add(element); + } + + private void Zoom_PropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == "Value") + { + decimal newValue = Zoom.Value ?? 0; + ChangeMappingZoom((double)newValue); + } + } + /// + /// Changes the zoom level on pdo mapping + /// + /// zoom level in percent + private void ChangeMappingZoom(double zoomPercent) + { + var zoom = zoomPercent / 100; + foreach (var column in _bitColumns) + { + column.Width = new GridLength(10 * zoom); + } + } +} diff --git a/EDSEditorGUI2/Views/DeviceView.axaml b/EDSEditorGUI2/Views/DeviceView.axaml new file mode 100644 index 00000000..707f1da9 --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceView.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + This is tab 3 content + + + + diff --git a/EDSEditorGUI2/Views/DeviceView.axaml.cs b/EDSEditorGUI2/Views/DeviceView.axaml.cs new file mode 100644 index 00000000..13758624 --- /dev/null +++ b/EDSEditorGUI2/Views/DeviceView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace EDSEditorGUI2.Views; + +public partial class DeviceView : UserControl +{ + public DeviceView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/Views/MainWindow.axaml b/EDSEditorGUI2/Views/MainWindow.axaml new file mode 100644 index 00000000..d2469cd4 --- /dev/null +++ b/EDSEditorGUI2/Views/MainWindow.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EDSEditorGUI2/Views/MainWindow.axaml.cs b/EDSEditorGUI2/Views/MainWindow.axaml.cs new file mode 100644 index 00000000..8fc2bd17 --- /dev/null +++ b/EDSEditorGUI2/Views/MainWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace EDSEditorGUI2.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/Views/ODIndexRangeView.axaml b/EDSEditorGUI2/Views/ODIndexRangeView.axaml new file mode 100644 index 00000000..08b0d548 --- /dev/null +++ b/EDSEditorGUI2/Views/ODIndexRangeView.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs b/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs new file mode 100644 index 00000000..f395a39a --- /dev/null +++ b/EDSEditorGUI2/Views/ODIndexRangeView.axaml.cs @@ -0,0 +1,88 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using DialogHostAvalonia; +using EDSEditorGUI2.Converter; +using LibCanOpen; +using System; +using System.Linq; + +namespace EDSEditorGUI2.Views; + +public partial class ODIndexRangeView : UserControl +{ + public ODIndexRangeView() + { + InitializeComponent(); + var values = Enum.GetNames(typeof(OdObject.Types.ObjectType)).Skip(1).ToArray(); + type.ItemsSource = values; + + grid.LoadingRow += GridLoadingRow; + } + + /// + /// Hides rows with indexes that is not in min&max range + /// + /// sender object + /// event param + private void GridLoadingRow(object? sender, DataGridRowEventArgs e) + { + if(e.Row.DataContext != null) + { + var dc = (System.Collections.Generic.KeyValuePair)e.Row.DataContext; + int index = int.Parse(dc.Key, System.Globalization.NumberStyles.HexNumber); + int min = Convert.ToInt32(MinIndex, 16); + int max = Convert.ToInt32(MaxIndex, 16); + e.Row.IsVisible = (min <= index && index <= max); + } + } + + public static readonly StyledProperty HeadingProperty = + AvaloniaProperty.Register(nameof(HeadingProperty)); + public string Heading + { + get { return GetValue(HeadingProperty); } + set { SetValue(HeadingProperty, value); HeadingText.Text = value; } + } + + public static readonly StyledProperty MinIndexProperty = + AvaloniaProperty.Register(nameof(MinIndexProperty)); + public string MinIndex + { + get { return GetValue(MinIndexProperty); } + set { SetValue(MinIndexProperty, value); } + } + + public static readonly StyledProperty MaxIndexProperty = + AvaloniaProperty.Register(nameof(MaxIndexProperty)); + public string MaxIndex + { + get { return GetValue(MaxIndexProperty); } + set { SetValue(MaxIndexProperty, value); } + } + + private async void AddIndex(object? sender, RoutedEventArgs e) + { + await DialogHost.Show(Resources["NewIndexDialog"]!, "NoAnimationDialogHost", OnDialogClosing); + } + + private void OnDialogClosing(object? sender, DialogClosingEventArgs e) + { + if (e.Parameter != null) + { + if (DataContext is ViewModels.DeviceOD dc && e.Parameter is NewIndexRequest param) + { + dc.AddIndex(param.Index, param.Name, param.Type); + } + } + } + + private async void RemoveIndex(object? sender, RoutedEventArgs e) + { + await DialogHost.Show(Resources["NewIndexDialog"]!, "NoAnimationDialogHost"); + } + + private void DataGrid_SelectionChanged(object? sender, Avalonia.Controls.SelectionChangedEventArgs e) + { + } +} \ No newline at end of file diff --git a/EDSEditorGUI2/app.manifest b/EDSEditorGUI2/app.manifest new file mode 100644 index 00000000..d88e268e --- /dev/null +++ b/EDSEditorGUI2/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/GUITests/.gitignore b/GUITests/.gitignore new file mode 100644 index 00000000..aaddf9f7 --- /dev/null +++ b/GUITests/.gitignore @@ -0,0 +1,3 @@ +/obj +/bin + diff --git a/GUITests/GUITests.csproj b/GUITests/GUITests.csproj new file mode 100644 index 00000000..499faeae --- /dev/null +++ b/GUITests/GUITests.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/GUITests/ViewModels_OdObjects.cs b/GUITests/ViewModels_OdObjects.cs new file mode 100644 index 00000000..25f779ff --- /dev/null +++ b/GUITests/ViewModels_OdObjects.cs @@ -0,0 +1,124 @@ +using EDSEditorGUI2.ViewModels; + +namespace GUITests +{ + public class ViewModels_OdObjects + { + private OdObject sut; + public ViewModels_OdObjects() + { + sut = new OdObject(); + sut.SubObjects.Add(new KeyValuePair("0", new OdSubObject + { + Name = "Highest sub-index supported", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned8, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Ro, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0x01" + })); + sut.SubObjects.Add(new KeyValuePair("1", new OdSubObject() + { + Name = "Sub Object 1", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + sut.SubObjects.Add(new KeyValuePair("2", new OdSubObject() + { + Name = "Sub Object 2", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + } + + [Fact] + public void AddSubEntry_VarType() + { + sut = new OdObject(); + sut.Type = LibCanOpen.OdObject.Types.ObjectType.Var; + sut.SubObjects.Add(new KeyValuePair("0", new OdSubObject + { + Name = "variableTest", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + sut.AddSubEntry(sut.SubObjects[0]); + Assert.Single(sut.SubObjects); + } + + [Fact] + public void AddSubEntry_RecordType() + { + sut.Type = LibCanOpen.OdObject.Types.ObjectType.Record; + sut.AddSubEntry(sut.SubObjects[1]); + Assert.Equal(4, sut.SubObjects.Count); + Assert.Equal("0x03", sut.SubObjects[0].Value.DefaultValue); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RemoveSubEntry_VarType(bool renumber) + { + sut = new OdObject(); + sut.Type = LibCanOpen.OdObject.Types.ObjectType.Var; + sut.SubObjects.Add(new KeyValuePair("0x01", new OdSubObject + { + Name = "variableTest", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + + var result = sut.RemoveSubEntry(sut.SubObjects[0], renumber); + Assert.False(result); + } + [Fact] + public void RemoveSubEntry_RecordType() + { + sut = new OdObject(); + sut.Type = LibCanOpen.OdObject.Types.ObjectType.Record; + sut.SubObjects.Add(new KeyValuePair("0x00", new OdSubObject + { + Name = "variableTest0", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0x01" + })); + sut.SubObjects.Add(new KeyValuePair("0x01", new OdSubObject + { + Name = "variableTest1", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + sut.SubObjects.Add(new KeyValuePair("0x02", new OdSubObject + { + Name = "variableTest2", + Type = LibCanOpen.OdSubObject.Types.DataType.Unsigned32, + Sdo = LibCanOpen.OdSubObject.Types.AccessSDO.Rw, + Pdo = LibCanOpen.OdSubObject.Types.AccessPDO.No, + Srdo = LibCanOpen.OdSubObject.Types.AccessSRDO.No, + DefaultValue = "0" + })); + + var result = sut.RemoveSubEntry(sut.SubObjects[1], false); + Assert.False(result); + } + } +} \ No newline at end of file