using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Media; using DTS.Common.Base; using DTS.Common.Enums.Sensors; using DTS.Common.Events; using DTS.Common.Interactivity; using DTS.Common.Interface; using DTS.Common.Interface.TestDefinition; using DTS.Common.Strings; using DTS.Common.Utils; using Prism.Events; using Prism.Regions; using Unity; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local // ReSharper disable CheckNamespace // ReSharper disable NotAccessedField.Local // ReSharper disable InconsistentNaming // ReSharper disable UnusedMember.Local // ReSharper disable RedundantDefaultMemberInitializer // ReSharper disable RedundantAssignment namespace DTS.Viewer.GraphList { public class GraphMainViewModel : BaseViewModel, IGraphMainViewModel { public IFilterView FilterView { get; private set; } public IGraphMainView View { get; set; } public IBaseViewModel Parent { get; set; } private IEventAggregator _eventAggregator { get; set; } private IUnityContainer _unityContainer { get; set; } private bool _showIsoCodes = false; public InteractionRequest NotificationRequest { get; private set; } public new InteractionRequest ConfirmationRequest { get; private set; } /// /// Creates a new instance of the GraphMainViewModel. /// /// The GraphMainView interface. /// The logical placeholder defined within the application's UI (in the shell or within views) into which views are displayed. /// The EventAggregator which allows different components to publish/subscribe to events without being coupled to each other. /// The unityContainer. public GraphMainViewModel(IGraphMainView view, IRegionManager regionManager, IEventAggregator eventAggregator, IUnityContainer unityContainer) : base(regionManager, eventAggregator, unityContainer) { View = view; View.DataContext = this; NotificationRequest = new InteractionRequest(); ConfirmationRequest = new InteractionRequest(); _eventAggregator = eventAggregator; _unityContainer = unityContainer; } #region Const private const string testChannels = "Test Channels"; private const string calculatedChannels = "Calculated Channels"; private const string graphChannels = "Graph Channels: "; #endregion #region Colors private readonly List _graphColors = new List { Colors.Transparent, Colors.Blue, Colors.Red, Colors.Green, Colors.BlueViolet, Colors.Brown, Colors.Aqua, Colors.Lime, Colors.Gray, Colors.YellowGreen, Colors.Black }; /// /// Assign unused color to selected/locked channel /// /// private Color GetNextColor() { var nextColor = Colors.Transparent; var channelCount = (LockedChannelList.Any() ? LockedChannelList.Count : 0) + (SelectedChannelList.Any() ? SelectedChannelList.Count : 0); if (channelCount >= _graphColors.Count) { var nextColorIndex = channelCount % _graphColors.Count + 1; if (nextColorIndex >= _graphColors.Count) { nextColorIndex = 1; } nextColor = _graphColors[nextColorIndex]; } else { var usedColors = new List(); usedColors.AddRange(LockedChannelList.Select(ch => ch.ChannelColor).ToList()); usedColors.AddRange(SelectedChannelList.Select(ch => ch.ChannelColor).ToList()); nextColor = _graphColors.Find(c1 => usedColors.TrueForAll(c2 => c2 != c1)); } return nextColor; } #endregion #region Methods public override void Initialize() { } private bool _lockedOnly = false; //only published locked channels public override void Initialize(object parameter) { Parent = (IBaseViewModel)parameter; if (Parent is IPSDReportMainViewModel) { _lockedOnly = true; } // only put checked channels in the report FilterView = GetFilterView(this); Subscribe(); } /// /// Reset all lists and publish changes /// private void CleanSelection() { LockedChannelList = new List(); ChannelList = new ObservableCollection(); FilteredChannelList = new ObservableCollection(); SelectedChannelList = new List(); _eventAggregator.GetEvent().Publish(new GraphClearNotificationArg { GraphClear = true, ParentVM = Parent }); PublishSelectedChannels(); SelectedGroupName = string.Empty; GC.Collect(); } /// /// Format the Channel Display Name based on the isoCode setting /// /// /// /// /// private string FormatChannelDisplayName(string channelName2, string channelDescriptionString, string isoCode) { return _showIsoCodes ? $"{channelName2} {channelDescriptionString} {Environment.NewLine} {isoCode}" : $"{channelName2} {channelDescriptionString}"; } //hold onto the test summaries for reloads private List testSummaries; /// /// Read all graphs, calculated channels and channels for the selected test /// and select and display first group or channel /// /// private void OnTestSummaryChanged(TestSummaryChangeNotificationArg arg) { if (Parent != arg?.ParentVM) return; var list = arg?.SummaryList; testSummaries = list; CleanSelection(); //clean every time, otherwise channels weren't being removed on test deselection var total = 0; if (list != null && list.Any()) { foreach (var l in list) { if (l.Channels.Any() && l.Channels.FirstOrDefault() == null) { // LOAD NOW! Utils.SetChannelInfo(l.TestMetadata, Path.GetDirectoryName(l.TestMetadata.TestRun.FilePath), DTS.Serialization.SliceRaw.File.PersistentChannel.GetIsoCode); l.Channels = l.TestMetadata.TestRun.Channels; l.Graphs = l.TestMetadata.TestSetup.TestGraphs; l.CalculatedChannels = l.TestMetadata.TestRun.CalculatedChannels; } foreach (var graph in l.Graphs) { //12017 Viewer shows "Graphs 1/2" when there is only 1 channel //this issue was caused by an included graph with no channels. if (graph.ChannelIds.Any()) { total++; } } if (null != l.CalculatedChannels) { total += l.CalculatedChannels.Count; } } } if (list != null && list.Count <= 0) return; var summaryList = list.ToList(); foreach (var ts in summaryList) { var testName = ts.Id + " " + ts.SetupName + " " + ts.DataType + " " + Utils.FormatTimeStamp(ts.TimeStamp); if (ts.Graphs.Count > 0) { foreach (var graph in ts.Graphs) { foreach (var gch in graph.Channels.OrderBy(gch => gch.AbsoluteDisplayOrder).ThenBy(gch => gch.Number)) { if (ChannelList.Any(x => x.Equals(gch))) continue; gch.Group = testName; gch.SubGroup = graphChannels + graph.Name; gch.IsGraphChannel = true; gch.GraphName = graph.Name; gch.ChannelDisplayName = gch.ChannelName2 + " " + gch.ChannelDescriptionString; gch.Parent = this; gch.ChannelColor = Colors.Transparent; gch.CanSelectChannel = false; gch.IsSelected = false; gch.IsLocked = false; gch.CanSelectChannel = true; //Select first group if (ChannelList.Count == 0) { SelectedGroupName = gch.SubGroup.Trim(); } ChannelList.Add(gch); } } } if ((ts.CalculatedChannels != null) && (ts.CalculatedChannels.Count > 0)) { foreach (var cch in ts.CalculatedChannels.OrderBy(cch => cch.AbsoluteDisplayOrder).ThenBy(cch => cch.Number)) { if (ChannelList.Any(x => x.Equals(cch))) continue; cch.Group = testName; cch.SubGroup = calculatedChannels; cch.IsGraphChannel = false; cch.GraphName = string.Empty; cch.ChannelDisplayName = cch.ChannelName2 + " " + cch.ChannelDescriptionString; cch.IsCalculatedChannel = true; cch.Parent = this; cch.ChannelColor = Colors.Transparent; cch.CanSelectChannel = false; cch.IsSelected = false; cch.IsLocked = false; cch.CanSelectChannel = true; //Or select first calculated channel if (ChannelList.Count == 0) { cch.IsSelected = true; } ChannelList.Add(cch); } } if (ts.Channels.Count > 0) { var tsChannels = ts.Channels.OrderBy(ch => ch.AbsoluteDisplayOrder).ThenBy(ch => ch.Number).ToList(); for (int i = 0; i < tsChannels.Count - 1; i++) { if (tsChannels[i].AbsoluteDisplayOrder == tsChannels[i + 1].AbsoluteDisplayOrder && tsChannels[i].Number == tsChannels[i + 1].Number) { switch (calibrationBehaviorSetting) { case CalibrationBehaviors.LinearIfAvailable: tsChannels.RemoveAt(i); i--; break; case CalibrationBehaviors.NonLinearIfAvailable: tsChannels.RemoveAt(i + 1); break; case CalibrationBehaviors.UseBothIfAvailable: tsChannels[i].ChannelDescriptionString += " (NonLinear)"; tsChannels[i + 1].ChannelDescriptionString += " (Linear)"; break; } } } total += tsChannels.Count; foreach (var ch in tsChannels) { if (ChannelList.Any(x => x.Equals(ch))) continue; ch.Group = testName; ch.SubGroup = testChannels; ch.IsGraphChannel = false; ch.GraphName = string.Empty; var channelDescription = ch.ChannelDescriptionString; if (SensorConstants.IsTestSpecificEmbedded(channelDescription)) { channelDescription = Strings.Table_NA; } ch.ChannelDisplayName = FormatChannelDisplayName(ch.ChannelName2, channelDescription, ch.IsoCode); ch.Parent = this; ch.ChannelColor = Colors.Transparent; ch.CanSelectChannel = false; ch.IsSelected = false; ch.IsLocked = false; ch.CanSelectChannel = true; //Or select first channel if (ChannelList.Count == 0) { ch.IsSelected = true; } ChannelList.Add(ch); } } } ChannelList.CollectionChanged += GraphList_CollectionChanged; FilteredChannelList = new ObservableCollection(ChannelList); TestChannelsTree.CollectionChanged += TestChannelsTree_CollectionChanged; _eventAggregator.GetEvent().Publish(new GraphLoadedCountNotificationArg { LoadedCount = total, ParentVM = Parent }); if (ChannelList.Any()) { var treeChannels = ChannelList.ToList(); TestChannelsTree = GetTestChannelsTree(treeChannels); if (!string.IsNullOrEmpty(SelectedGroupName)) { var group = (from t in TestChannelsTree from g in t.Groups where g.Name == SelectedGroupName.Replace(graphChannels, string.Empty) select g).FirstOrDefault(); if (group != null) { AddSelectedGroup(group); } } } IsFilterEnabled = _channelList.Count > 0; } /// /// Check changes to Test Modifications to see whether we need to withhold user input /// private void OnTestModificationsChanged(ITestModificationModel testModificationModel) { if (testModificationModel != null) { TestModified = testModificationModel.IsModified; } } /// /// Subscribe for events /// private void Subscribe() { _eventAggregator.GetEvent().Subscribe(OnRaiseNotification); _eventAggregator.GetEvent().Subscribe(OnFilterChanged); _eventAggregator.GetEvent().Subscribe(OnTestSummaryChanged); //_eventAggregator.GetEvent().Subscribe(OnReadCompletedNotification); // commented out: was noted "It does not work" _eventAggregator.GetEvent().Subscribe(OnTestModificationsChanged); _eventAggregator.GetEvent().Subscribe(OnCalibrationBehaviorSettingChanged); _eventAggregator.GetEvent().Subscribe(OnCalibrationBehaviorSettableInViewerChanged); _eventAggregator.GetEvent().Subscribe(OnChannelCodesViewChangedEvent); } private void OnCalibrationBehaviorSettableInViewerChanged(bool cbSettable) { } private void OnChannelCodesViewChangedEvent(DTS.Common.Enums.IsoViewMode viewMode) => _showIsoCodes = (viewMode == Common.Enums.IsoViewMode.ISOAndUserCode || viewMode == Common.Enums.IsoViewMode.ISOOnly); private CalibrationBehaviors calibrationBehaviorSetting { get; set; } = CalibrationBehaviors.NonLinearIfAvailable; private void OnCalibrationBehaviorSettingChanged(CalibrationBehaviors cb) { calibrationBehaviorSetting = cb; if (null != testSummaries) { OnTestSummaryChanged(new TestSummaryChangeNotificationArg { SummaryList = testSummaries, ParentVM = Parent }); } } #region Filter /// /// Filter Data Event /// /// Filter value public void OnFilterChanged(FilterParameterArgs args) { if (((IFilterViewModel)FilterView.DataContext).Parent == this) { FilteredChannelList = string.IsNullOrEmpty(args.Param) ? new ObservableCollection(ChannelList) : new ObservableCollection((from x in ChannelList where x.ChannelDisplayName.ToLower().Contains(args.Param.ToLower()) select x).ToList()); } } #endregion Filter #region Override /// /// Private Event handler for RaiseNotification event. /// private void OnRaiseNotification(NotificationContentEventArgs eventArgsWithTitle) { // Notification object expects a NotificationContentEventArgsWithoutTitle object and a Title string. var eventArgsWithoutTitle = new NotificationContentEventArgs(eventArgsWithTitle.Message, eventArgsWithTitle.MessageDetails, eventArgsWithTitle.Image); NotificationRequest.Raise(new Notification { Content = eventArgsWithoutTitle, Title = eventArgsWithTitle.Title }); } /// /// ? I thik it's not beed used /// public override void Activated() { var fp = new FilterParameterArgs { Param = string.Empty, Requester = this }; _eventAggregator.GetEvent().Publish(fp); } #endregion Override /// /// Publish Locked Channels /// /// /// Publish Selected and/or locked Channels /// public void PublishSelectedChannels() { var list = new List(); if (LockedChannelList.Any()) list.AddRange(LockedChannelList); if (!_lockedOnly && SelectedChannelList.Any()) list.AddRange(SelectedChannelList); var count = list.Count; _eventAggregator.GetEvent().Publish(new GraphSelectedChannelCountNotificationArg { SelectedChannelCount = count, ParentVM = Parent }); _eventAggregator.GetEvent().Publish(new GraphSelectedChannelsNotificationArg { SelectedChannels = list, ParentVM = Parent }); } /// /// Load Filter View /// /// /// private IFilterView GetFilterView(IBaseViewModel parent) { var view = _unityContainer.Resolve(); var viewModel = _unityContainer.Resolve(); view.DataContext = viewModel; viewModel.Initialize(parent); return view; } #endregion #region Reset Functions private void ResetSelectedChannelColor() { foreach (var ch in SelectedChannelList) { if (ch.IsLocked) continue; ch.ChannelColor = Colors.Transparent; ch.CanSelectChannel = true; } } /// /// Reset all selected channels prior to selecting new channel or group /// private void ResetSelected() { ResetSelected(string.Empty); } private void ResetSelected(string groupName) { foreach (var test in TestChannelsTree) { foreach (var group in test.Groups) { if (!string.IsNullOrEmpty(groupName) && group.Name == groupName) continue; group.IsSelected = false; foreach (var channel in group.Channels) { if (channel.IsLocked) continue; channel.CanSelectChannel = true; channel.ChannelColor = Colors.Transparent; } } } } #endregion Reset Functions #region Add Selected/Locked Channel #region Locked Channel public void AddLockedGroupChannels(string testName, string groupName, List channels, bool isLocked) { _eventAggregator.GetEvent().Publish(true); foreach (var channel in channels) { if (isLocked) { if (SelectedChannelList.Contains(channel)) { SelectedChannelList.Remove(channel); } if (LockedChannelList.Contains(channel)) continue; channel.CanSelectChannel = false; channel.IsLocked = true; channel.CanSelectChannel = true; LockedChannelList.Add(channel); if (channel.ChannelColor == Colors.Transparent) { channel.ChannelColor = GetNextColor(); } } else { if (!LockedChannelList.Contains(channel)) continue; LockedChannelList.Remove(channel); } } PublishSelectedChannels(); if (!LockedChannelList.Any() && !SelectedChannelList.Any()) { var group = (from t in TestChannelsTree from g in t.Groups where g.TestName == testName && g.Name == groupName select g).FirstOrDefault(); if (group != null) { group.IsSelected = true; } } else { if (SelectedChannelList.Any()) { PublishSelectedChannels(); } } UpdateChannelLocks(); _eventAggregator.GetEvent().Publish(false); } public void AddLockedChannel(ITestChannel channel, bool isLocked) { _eventAggregator.GetEvent().Publish(true); if (isLocked) { if (SelectedChannelList.Contains(channel)) { SelectedChannelList.Remove(channel); } if (!LockedChannelList.Contains(channel)) { channel.CanSelectChannel = false; channel.IsLocked = true; channel.CanSelectChannel = true; LockedChannelList.Add(channel); if (channel.ChannelColor == Colors.Transparent) { channel.ChannelColor = GetNextColor(); } } } else { if (LockedChannelList.Contains(channel)) { LockedChannelList.Remove(channel); } // if there are no selected or locked channels add unlocked channel to selected list if (!SelectedChannelList.Any()) { if (!LockedChannelList.Exists(x => x.IsSelected)) { AddSelectedChannel(channel, true); } } } UpdateChannelLocks(); PublishSelectedChannels(); _eventAggregator.GetEvent().Publish(false); } private const int MAX_LOCKED_CHANNELS = 8; private void UpdateChannelLocks() { var canLock = LockedChannelList.Count < MAX_LOCKED_CHANNELS; foreach (var treeChannel in TestChannelsTree) { foreach (var group in treeChannel.Groups) { if (group.IsGraph) { if (group.IsLocked) { continue; } group.CanLock = group.Channels.Count + LockedChannelList.Count <= MAX_LOCKED_CHANNELS; } foreach (var channel in group.Channels) { if (channel.IsLocked) { continue; } channel.CanLock = canLock; } } } } #endregion Locked Channel #region Selected Channel public void AddSelectedGroupChannels(string groupName, List channels) { _eventAggregator.GetEvent().Publish(true); ResetSelectedChannelColor(); ResetSelected(groupName); SelectedChannelList = new List(); foreach (var channel in channels) { SelectedChannelList.Add(channel); if (channel.ChannelColor == Colors.Transparent) { channel.ChannelColor = GetNextColor(); } } PublishSelectedChannels(); _eventAggregator.GetEvent().Publish(false); } public void AddSelectedGroup(TestGroup group) { _eventAggregator.GetEvent().Publish(true); group.IsSelected = true; _eventAggregator.GetEvent().Publish(false); } /// /// Add selected channel to the list or select a group (graph) of channels /// /// /// public void AddSelectedChannel(ITestChannel channel, bool reset) { if (channel.IsLocked && LockedChannelList.Contains(channel)) { ResetSelectedChannelColor(); SelectedChannelList = new List(); PublishSelectedChannels(); return; } if (channel.IsSelected && SelectedChannelList.Contains(channel)) return; if (channel.IsGraphChannel) { var group = (from t in TestChannelsTree from g in t.Groups where g.TestName == channel.Group && g.Name == channel.GraphName select g).FirstOrDefault(); if (group != null) { AddSelectedGroup(group); } } else { _eventAggregator.GetEvent().Publish(true); if (reset) { SelectedChannelList = new List(); ResetSelected(); SelectedGroupName = string.Empty; } SelectedChannelList.Add(channel); if (channel.ChannelColor == Colors.Transparent) { channel.ChannelColor = GetNextColor(); } PublishSelectedChannels(); _eventAggregator.GetEvent().Publish(false); } } public void AddSelectedChannel(ITestChannel channel) { if (channel.IsLocked && LockedChannelList.Contains(channel)) { //ResetSelected(); SelectedChannelList = new List(); PublishSelectedChannels(); return; } if (channel.IsSelected && SelectedChannelList.Contains(channel)) return; if (channel.IsGraphChannel) { var group = (from t in TestChannelsTree from g in t.Groups where g.TestName == channel.Group && g.Name == channel.GraphName select g).FirstOrDefault(); if (group != null) { AddSelectedGroup(group); } } else { _eventAggregator.GetEvent().Publish(true); SelectedChannelList = new List(); ResetSelected(); SelectedGroupName = string.Empty; SelectedChannelList.Add(channel); if (channel.ChannelColor == Colors.Transparent) { channel.ChannelColor = GetNextColor(); } PublishSelectedChannels(); _eventAggregator.GetEvent().Publish(false); } } #endregion Selected Channel #endregion Add Selected/Locked Channel #region ContextRegion public object ContextGraphMainRegion { get => ((GraphMainView)View).GraphMainRegion.DataContext; set { ((GraphMainView)View).GraphMainRegion.DataContext = value; OnPropertyChanged("ContextGraphMainRegion"); } } #endregion #region Properties private bool _isFilterEnabled = false; public bool IsFilterEnabled { get => _isFilterEnabled; set { _isFilterEnabled = value; OnPropertyChanged("IsFilterEnabled"); } } #region Selected/Locked Group private string _selectedGroupName = string.Empty; public string SelectedGroupName { get => _selectedGroupName; set { _selectedGroupName = value; OnPropertyChanged("SelectedGroupName"); } } private string _lockedGroupName = string.Empty; public string LockedGroupName { get => _lockedGroupName; set { _lockedGroupName = value; OnPropertyChanged("LockedGroupName"); } } #endregion Selected/Locked Group #region Lists private List _lockedChannelList = new List(); public List LockedChannelList { get => _lockedChannelList; set { _lockedChannelList = value; OnPropertyChanged("LockedChannelList"); } } private List _selectedChannelList = new List(); public List SelectedChannelList { get => _selectedChannelList; set { _selectedChannelList = value; OnPropertyChanged("SelectedChannelList"); } } #endregion Lists #region ObservableCollection private ObservableCollection _channelList = new ObservableCollection(); public ObservableCollection ChannelList { get => _channelList; set { _channelList = value; IsFilterEnabled = _channelList.Count > 0; FilteredChannelList = new ObservableCollection(_channelList); OnPropertyChanged("ChannelList"); } } private ObservableCollection _filteredChannelList = new ObservableCollection(); public ObservableCollection FilteredChannelList { get => _filteredChannelList; set { _filteredChannelList = value; TestChannelsTree = GetTestChannelsTree(_filteredChannelList.ToList()); OnPropertyChanged("FilteredChannelList"); } } private ObservableCollection _testChannelsTree = new ObservableCollection(); /// /// Main treeview object /// public ObservableCollection TestChannelsTree { get => _testChannelsTree; set { _testChannelsTree = value; OnPropertyChanged("TestChannelsTree"); } } #endregion ObservableCollection #region ObservableCollection Extension public void TestChannelsTree_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { foreach (INotifyPropertyChanged item in e.OldItems) item.PropertyChanged -= TestChannelsTree_PropertyChanged; } if (e.NewItems != null) { foreach (INotifyPropertyChanged item in e.NewItems) { item.PropertyChanged += TestChannelsTree_PropertyChanged; } } } private void TestChannelsTree_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != "TestChannelsTree") { } } public void GraphList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { foreach (INotifyPropertyChanged item in e.OldItems) item.PropertyChanged -= GraphList_PropertyChanged; } if (e.NewItems != null) { foreach (INotifyPropertyChanged item in e.NewItems) { item.PropertyChanged += GraphList_PropertyChanged; } } } private void GraphList_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != "ChannelList") { } } #endregion ObservableCollection Extension #region PropertyChanged /// ///Occurs when a property value changes. /// public new event PropertyChangedEventHandler PropertyChanged; private new void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion PropertyChanged /// /// Gets the HeaderInfo. /// public string HeaderInfo => "GraphRegion"; public new bool IsBusy { get; set; } public new bool IsDirty { get; set; } private bool _testModified = false; public bool TestModified { get => _testModified; set { _testModified = value; OnPropertyChanged("TestModified"); } } #endregion /// /// Build the tree form list /// /// Test Channel list /// tree private ObservableCollection GetTestChannelsTree(List list) { if (!list.Any()) return new ObservableCollection(); var result = (from t in list group t.TestSetupName by t.Group into test select new TreeViewChannels { Name = test.Key, Groups = new ObservableCollection(from t in list where t.Group == test.Key group t.Group by t.SubGroup into g select new TestGroup { TestName = test.Key, DisplayName = g.Key + " [ " + list.Count(ch => ch.Group == test.Key && ch.SubGroup == g.Key) + " Channels]", Path = test.Key + "|" + g.Key + " [ " + list.Count(ch => ch.Group == test.Key && ch.SubGroup == g.Key) + " Channels]", Name = g.Key.Replace(graphChannels, string.Empty), IsGraph = g.Key.StartsWith("Graph"), Channels = new ObservableCollection(list.Where(ch => ch.Group == test.Key && ch.SubGroup == g.Key).OrderBy(ch => ch.AbsoluteDisplayOrder).ThenBy(ch => ch.Number)), Parent = this }) }).ToList(); return new ObservableCollection(result); } #region Commands #endregion Commands } }