diff --git a/XLAB.DATA/XLAB.DATA.csproj b/XLAB.DATA/XLAB.DATA.csproj
index bea281e..5026a53 100644
--- a/XLAB.DATA/XLAB.DATA.csproj
+++ b/XLAB.DATA/XLAB.DATA.csproj
@@ -1,5 +1,5 @@
-
+
Debug
@@ -12,7 +12,6 @@
512
{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
4
- true
true
@@ -32,6 +31,14 @@
4
+
+ ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll
+ True
+
+
+ ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll
+ True
+
@@ -148,4 +155,4 @@
-
\ No newline at end of file
+
diff --git a/XLAB.DATA/packages.config b/XLAB.DATA/packages.config
index 8408f24..1be0099 100644
--- a/XLAB.DATA/packages.config
+++ b/XLAB.DATA/packages.config
@@ -1,4 +1,5 @@
+
-
\ No newline at end of file
+
diff --git a/XLAB.sln b/XLAB.sln
new file mode 100644
index 0000000..50ba80c
--- /dev/null
+++ b/XLAB.sln
@@ -0,0 +1,26 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLAB.DATA", "XLAB.DATA\XLAB.DATA.csproj", "{AE0E35D7-DFA4-4150-9889-255043B232BB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLAB", "XLAB\XLAB.csproj", "{B8DAAB84-777A-4274-8452-E602DB1AF587}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE0E35D7-DFA4-4150-9889-255043B232BB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B8DAAB84-777A-4274-8452-E602DB1AF587}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/XLAB/App.config b/XLAB/App.config
index 193aecc..653a0be 100644
--- a/XLAB/App.config
+++ b/XLAB/App.config
@@ -1,6 +1,14 @@
-
+
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
diff --git a/XLAB/App.xaml.cs b/XLAB/App.xaml.cs
index 7effee5..44428d3 100644
--- a/XLAB/App.xaml.cs
+++ b/XLAB/App.xaml.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Configuration;
-using System.Data;
-using System.Linq;
-using System.Threading.Tasks;
+using System.Globalization;
+using System.Threading;
using System.Windows;
+using System.Windows.Markup;
namespace XLAB
{
@@ -13,5 +10,23 @@ namespace XLAB
///
public partial class App : Application
{
+ public App()
+ {
+ ApplyRussianCulture();
+ }
+
+ private static void ApplyRussianCulture()
+ {
+ var culture = new CultureInfo("ru-RU");
+
+ CultureInfo.DefaultThreadCurrentCulture = culture;
+ CultureInfo.DefaultThreadCurrentUICulture = culture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+
+ FrameworkElement.LanguageProperty.OverrideMetadata(
+ typeof(FrameworkElement),
+ new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(culture.IetfLanguageTag)));
+ }
}
}
diff --git a/XLAB/CreateDocumentWindow.xaml b/XLAB/CreateDocumentWindow.xaml
new file mode 100644
index 0000000..9f83c7a
--- /dev/null
+++ b/XLAB/CreateDocumentWindow.xaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB/CreateDocumentWindow.xaml.cs b/XLAB/CreateDocumentWindow.xaml.cs
new file mode 100644
index 0000000..a074105
--- /dev/null
+++ b/XLAB/CreateDocumentWindow.xaml.cs
@@ -0,0 +1,20 @@
+using System.Windows;
+
+namespace XLAB
+{
+ public partial class CreateDocumentWindow : Window
+ {
+ internal CreateDocumentWindow(CreateDocumentWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModelOnCloseRequested;
+ }
+
+ private void ViewModelOnCloseRequested(object sender, bool? dialogResult)
+ {
+ DialogResult = dialogResult;
+ Close();
+ }
+ }
+}
diff --git a/XLAB/CreateDocumentWindowViewModel.cs b/XLAB/CreateDocumentWindowViewModel.cs
new file mode 100644
index 0000000..1e7b93a
--- /dev/null
+++ b/XLAB/CreateDocumentWindowViewModel.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ internal sealed class CreateDocumentWindowViewModel : ObservableObject
+ {
+ private DateTime? _acceptedOn;
+ private string _documentNumber;
+ private string _validationMessage;
+
+ public CreateDocumentWindowViewModel(DocumentEditorResult seed)
+ {
+ _acceptedOn = seed != null ? seed.AcceptedOn : DateTime.Today;
+ _documentNumber = seed != null ? seed.DocumentNumber : string.Empty;
+
+ ConfirmCommand = new RelayCommand(Confirm);
+ CancelCommand = new RelayCommand(Cancel);
+ }
+
+ public event EventHandler CloseRequested;
+
+ public DateTime? AcceptedOn
+ {
+ get { return _acceptedOn; }
+ set { SetProperty(ref _acceptedOn, value); }
+ }
+
+ public ICommand CancelCommand { get; private set; }
+
+ public ICommand ConfirmCommand { get; private set; }
+
+ public string DocumentNumber
+ {
+ get { return _documentNumber; }
+ set { SetProperty(ref _documentNumber, value); }
+ }
+
+ public string ValidationMessage
+ {
+ get { return _validationMessage; }
+ private set { SetProperty(ref _validationMessage, value); }
+ }
+
+ public DocumentEditorResult ToResult()
+ {
+ return new DocumentEditorResult
+ {
+ DocumentNumber = DocumentNumber == null ? string.Empty : DocumentNumber.Trim(),
+ AcceptedOn = AcceptedOn.HasValue ? AcceptedOn.Value : DateTime.Today,
+ IssuedOn = null
+ };
+ }
+
+ private void Cancel(object parameter)
+ {
+ RaiseCloseRequested(false);
+ }
+
+ private void Confirm(object parameter)
+ {
+ if (string.IsNullOrWhiteSpace(DocumentNumber))
+ {
+ ValidationMessage = "Введите номер ПСВ.";
+ return;
+ }
+
+ if (!AcceptedOn.HasValue)
+ {
+ ValidationMessage = "Укажите дату приемки.";
+ return;
+ }
+
+ ValidationMessage = string.Empty;
+ RaiseCloseRequested(true);
+ }
+
+ private void RaiseCloseRequested(bool? dialogResult)
+ {
+ var handler = CloseRequested;
+ if (handler != null)
+ {
+ handler(this, dialogResult);
+ }
+ }
+ }
+}
diff --git a/XLAB/DialogService.cs b/XLAB/DialogService.cs
new file mode 100644
index 0000000..91e40e7
--- /dev/null
+++ b/XLAB/DialogService.cs
@@ -0,0 +1,57 @@
+using System.Windows;
+
+namespace XLAB
+{
+ internal interface IDialogService
+ {
+ DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed);
+
+ bool Confirm(string message);
+
+ void ShowError(string message);
+
+ void ShowInfo(string message);
+
+ void ShowWarning(string message);
+ }
+
+ internal sealed class DialogService : IDialogService
+ {
+ private readonly Window _owner;
+
+ public DialogService(Window owner)
+ {
+ _owner = owner;
+ }
+
+ public DocumentEditorResult ShowCreateDocumentDialog(DocumentEditorResult seed)
+ {
+ var viewModel = new CreateDocumentWindowViewModel(seed);
+ var window = new CreateDocumentWindow(viewModel);
+ window.Owner = _owner;
+
+ var result = window.ShowDialog();
+ return result.HasValue && result.Value ? viewModel.ToResult() : null;
+ }
+
+ public bool Confirm(string message)
+ {
+ return MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
+ }
+
+ public void ShowError(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ public void ShowInfo(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public void ShowWarning(string message)
+ {
+ MessageBox.Show(_owner, message, "ПСВ", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+}
diff --git a/XLAB/MainWindow.xaml b/XLAB/MainWindow.xaml
new file mode 100644
index 0000000..7f0c807
--- /dev/null
+++ b/XLAB/MainWindow.xaml
@@ -0,0 +1,265 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XLAB/MainWindow.xaml.cs b/XLAB/MainWindow.xaml.cs
new file mode 100644
index 0000000..9265c71
--- /dev/null
+++ b/XLAB/MainWindow.xaml.cs
@@ -0,0 +1,33 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ public partial class MainWindow : Window
+ {
+ private readonly MainWindowViewModel _viewModel;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ _viewModel = new MainWindowViewModel(new PsvDataService(), new DialogService(this));
+ DataContext = _viewModel;
+ }
+
+ private void DocumentListItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var item = sender as ListBoxItem;
+ if (item != null)
+ {
+ item.IsSelected = true;
+ item.Focus();
+ }
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ await _viewModel.InitializeAsync();
+ }
+ }
+}
diff --git a/XLAB/MainWindowViewModel.cs b/XLAB/MainWindowViewModel.cs
new file mode 100644
index 0000000..264dc4c
--- /dev/null
+++ b/XLAB/MainWindowViewModel.cs
@@ -0,0 +1,645 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Data;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ internal sealed class MainWindowViewModel : ObservableObject
+ {
+ private readonly List _draftDocuments;
+ private readonly IDialogService _dialogService;
+ private readonly PsvDataService _service;
+ private string _documentFilterText;
+ private string _documentNumberEditor;
+ private string _documentStatusText;
+ private string _groupDetailFilterText;
+ private string _headerCustomerName;
+ private string _headerDepartmentName;
+ private DateTime? _headerIssuedOn;
+ private DateTime? _headerReceivedOn;
+ private bool _isBusy;
+ private string _lineStatusText;
+ private PsvDocumentSummary _selectedDocument;
+ private PsvDocumentGroupSummary _selectedDocumentGroup;
+
+ public MainWindowViewModel(PsvDataService service, IDialogService dialogService)
+ {
+ _service = service;
+ _dialogService = dialogService;
+ _draftDocuments = new List();
+
+ Documents = new ObservableCollection();
+ DocumentLines = new ObservableCollection();
+ DocumentGroupSummaries = new ObservableCollection();
+
+ DocumentsView = CollectionViewSource.GetDefaultView(Documents);
+ DocumentsView.Filter = FilterDocuments;
+
+ DocumentLinesView = CollectionViewSource.GetDefaultView(DocumentLines);
+ DocumentLinesView.Filter = FilterDocumentLines;
+
+ AddDocumentCommand = new RelayCommand(delegate { AddDocument(); }, delegate { return !IsBusy; });
+ DeleteDocumentCommand = new RelayCommand(delegate { DeleteDocumentAsync(); }, delegate { return !IsBusy && SelectedDocument != null; });
+ RefreshDocumentsCommand = new RelayCommand(delegate { RefreshDocumentsAsync(null); }, delegate { return !IsBusy; });
+ SaveDocumentHeaderCommand = new RelayCommand(delegate { SaveDocumentHeaderAsync(); }, delegate { return !IsBusy && SelectedDocument != null && !SelectedDocument.IsDraft; });
+
+ DocumentStatusText = "Готово.";
+ LineStatusText = "Документ не выбран.";
+ }
+
+ public ICommand AddDocumentCommand { get; private set; }
+
+ public string DocumentFilterText
+ {
+ get { return _documentFilterText; }
+ set
+ {
+ if (SetProperty(ref _documentFilterText, value))
+ {
+ DocumentsView.Refresh();
+ }
+ }
+ }
+
+ public string DocumentNumberEditor
+ {
+ get { return _documentNumberEditor; }
+ set { SetProperty(ref _documentNumberEditor, value); }
+ }
+
+ public string DocumentStatusText
+ {
+ get { return _documentStatusText; }
+ private set { SetProperty(ref _documentStatusText, value); }
+ }
+
+ public ObservableCollection DocumentLines { get; private set; }
+
+ public ObservableCollection DocumentGroupSummaries { get; private set; }
+
+ public ICollectionView DocumentLinesView { get; private set; }
+
+ public ObservableCollection Documents { get; private set; }
+
+ public ICollectionView DocumentsView { get; private set; }
+
+ public ICommand DeleteDocumentCommand { get; private set; }
+
+ public string GroupDetailFilterText
+ {
+ get { return _groupDetailFilterText; }
+ set
+ {
+ if (SetProperty(ref _groupDetailFilterText, value))
+ {
+ RefreshDocumentLinesView();
+ }
+ }
+ }
+
+ public string HeaderCustomerName
+ {
+ get { return _headerCustomerName; }
+ private set { SetProperty(ref _headerCustomerName, value); }
+ }
+
+ public string HeaderDepartmentName
+ {
+ get { return _headerDepartmentName; }
+ private set { SetProperty(ref _headerDepartmentName, value); }
+ }
+
+ public DateTime? HeaderIssuedOn
+ {
+ get { return _headerIssuedOn; }
+ set { SetProperty(ref _headerIssuedOn, value); }
+ }
+
+ public DateTime? HeaderReceivedOn
+ {
+ get { return _headerReceivedOn; }
+ set { SetProperty(ref _headerReceivedOn, value); }
+ }
+
+ public bool IsBusy
+ {
+ get { return _isBusy; }
+ private set
+ {
+ if (SetProperty(ref _isBusy, value))
+ {
+ RaiseCommandStates();
+ }
+ }
+ }
+
+ public string LineStatusText
+ {
+ get { return _lineStatusText; }
+ private set { SetProperty(ref _lineStatusText, value); }
+ }
+
+ public ICommand RefreshDocumentsCommand { get; private set; }
+
+ public ICommand SaveDocumentHeaderCommand { get; private set; }
+
+ public PsvDocumentSummary SelectedDocument
+ {
+ get { return _selectedDocument; }
+ set
+ {
+ if (SetProperty(ref _selectedDocument, value))
+ {
+ FillHeaderFromSelection();
+ RaiseCommandStates();
+ LoadSelectedDocumentAsync();
+ }
+ }
+ }
+
+ public PsvDocumentGroupSummary SelectedDocumentGroup
+ {
+ get { return _selectedDocumentGroup; }
+ set
+ {
+ if (SetProperty(ref _selectedDocumentGroup, value))
+ {
+ RefreshDocumentLinesView();
+ }
+ }
+ }
+
+ public async Task InitializeAsync()
+ {
+ await RefreshDocumentsCoreAsync(null);
+ }
+
+ private void AddDocument()
+ {
+ if (IsBusy)
+ {
+ return;
+ }
+
+ var request = _dialogService.ShowCreateDocumentDialog(new DocumentEditorResult
+ {
+ AcceptedOn = DateTime.Today,
+ IssuedOn = null,
+ DocumentNumber = string.Empty
+ });
+
+ if (request == null)
+ {
+ return;
+ }
+
+ request.DocumentNumber = request.DocumentNumber == null ? string.Empty : request.DocumentNumber.Trim();
+ request.IssuedOn = null;
+
+ if (string.IsNullOrWhiteSpace(request.DocumentNumber))
+ {
+ _dialogService.ShowWarning("Введите номер ПСВ.");
+ return;
+ }
+
+ if (Documents.Any(delegate(PsvDocumentSummary document)
+ {
+ return string.Equals(document.DocumentNumber, request.DocumentNumber, StringComparison.OrdinalIgnoreCase);
+ }))
+ {
+ _dialogService.ShowWarning("ПСВ с таким номером уже есть в списке.");
+ return;
+ }
+
+ var draft = new PsvDocumentSummary
+ {
+ DocumentNumber = request.DocumentNumber,
+ AcceptedOn = request.AcceptedOn,
+ IssuedOn = null,
+ CustomerName = "Черновик текущего сеанса",
+ DepartmentName = string.Empty,
+ ItemCount = 0,
+ IssuedCount = 0,
+ PassedCount = 0,
+ FailedCount = 0,
+ IsDraft = true
+ };
+
+ _draftDocuments.Add(draft);
+ InsertDraftIntoCollection(draft);
+ DocumentsView.Refresh();
+ SelectedDocument = draft;
+ DocumentStatusText = string.Format("Документов: {0}.", Documents.Count);
+ }
+
+ private void ClearCollections(ObservableCollection collection)
+ {
+ collection.Clear();
+ }
+
+ private void ClearHeader()
+ {
+ DocumentNumberEditor = string.Empty;
+ HeaderReceivedOn = null;
+ HeaderIssuedOn = null;
+ HeaderCustomerName = string.Empty;
+ HeaderDepartmentName = string.Empty;
+ }
+
+ private void DeleteDocumentAsync()
+ {
+ if (SelectedDocument == null)
+ {
+ return;
+ }
+
+ var selectedDocument = SelectedDocument;
+ if (!_dialogService.Confirm(string.Format("Удалить ПСВ \"{0}\"?", selectedDocument.DocumentNumber)))
+ {
+ return;
+ }
+
+ if (selectedDocument.IsDraft)
+ {
+ DeleteDraftDocument(selectedDocument);
+ return;
+ }
+
+ ExecuteBusyOperationAsync(async delegate
+ {
+ var result = await Task.Run(delegate { return _service.DeleteDocument(selectedDocument.DocumentNumber); });
+ await RefreshDocumentsCoreAsync(null);
+ _dialogService.ShowInfo(
+ string.Format(
+ "Удалено строк EKZMKFCTVL: {0}. Удалено строк EKZMK: {1}. Удалено связанных DMS: {2}.",
+ result.DeletedEkzMkFctvlCount,
+ result.DeletedEkzMkCount,
+ result.DeletedDmsCount));
+ });
+ }
+
+ private void DeleteDraftDocument(PsvDocumentSummary draft)
+ {
+ _draftDocuments.RemoveAll(delegate(PsvDocumentSummary item)
+ {
+ return string.Equals(item.DocumentNumber, draft.DocumentNumber, StringComparison.OrdinalIgnoreCase);
+ });
+
+ Documents.Remove(draft);
+ DocumentsView.Refresh();
+
+ if (Documents.Count > 0)
+ {
+ SelectedDocument = Documents[0];
+ }
+ else
+ {
+ SelectedDocument = null;
+ }
+
+ DocumentStatusText = string.Format("Документов: {0}.", Documents.Count);
+ }
+
+ private void ExecuteBusyOperationAsync(Func operation)
+ {
+ ExecuteBusyOperationCoreAsync(operation);
+ }
+
+ private async void ExecuteBusyOperationCoreAsync(Func operation)
+ {
+ if (IsBusy)
+ {
+ return;
+ }
+
+ try
+ {
+ IsBusy = true;
+ await operation();
+ }
+ catch (Exception ex)
+ {
+ _dialogService.ShowError(ex.Message);
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private bool FilterDocuments(object item)
+ {
+ var document = item as PsvDocumentSummary;
+ if (document == null)
+ {
+ return false;
+ }
+
+ if (document.IssuedOn.HasValue)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(DocumentFilterText)
+ && !Contains(document.DocumentNumber, DocumentFilterText)
+ && !Contains(document.CustomerName, DocumentFilterText))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool FilterDocumentLines(object item)
+ {
+ var line = item as PsvDocumentLine;
+ if (line == null || SelectedDocumentGroup == null)
+ {
+ return false;
+ }
+
+ if (!SelectedDocumentGroup.Matches(line))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(GroupDetailFilterText))
+ {
+ return true;
+ }
+
+ return Contains(line.SerialNumber, GroupDetailFilterText);
+ }
+
+ private void FillHeaderFromSelection()
+ {
+ if (SelectedDocument == null)
+ {
+ ClearHeader();
+ return;
+ }
+
+ DocumentNumberEditor = SelectedDocument.DocumentNumber;
+ HeaderReceivedOn = SelectedDocument.AcceptedOn;
+ HeaderIssuedOn = SelectedDocument.IssuedOn;
+ HeaderCustomerName = SelectedDocument.CustomerName;
+ HeaderDepartmentName = SelectedDocument.DepartmentName;
+ }
+
+ private PsvDocumentGroupSummary FindMatchingGroup(PsvDocumentGroupSummary group)
+ {
+ if (group == null)
+ {
+ return null;
+ }
+
+ return DocumentGroupSummaries.FirstOrDefault(delegate(PsvDocumentGroupSummary current)
+ {
+ return string.Equals(current.InstrumentType ?? string.Empty, group.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(current.RangeText ?? string.Empty, group.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(current.RegistryNumber ?? string.Empty, group.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ private void InsertDraftIntoCollection(PsvDocumentSummary draft)
+ {
+ var insertIndex = 0;
+ while (insertIndex < Documents.Count
+ && (Documents[insertIndex].AcceptedOn ?? DateTime.MinValue) >= (draft.AcceptedOn ?? DateTime.MinValue))
+ {
+ insertIndex++;
+ }
+
+ Documents.Insert(insertIndex, draft);
+ }
+
+ private async void LoadSelectedDocumentAsync()
+ {
+ if (SelectedDocument == null)
+ {
+ ClearCollections(DocumentLines);
+ ClearCollections(DocumentGroupSummaries);
+ SelectedDocumentGroup = null;
+ LineStatusText = "Документ не выбран.";
+ return;
+ }
+
+ if (SelectedDocument.IsDraft)
+ {
+ ClearCollections(DocumentLines);
+ ClearCollections(DocumentGroupSummaries);
+ SelectedDocumentGroup = null;
+ LineStatusText = "Черновик текущего сеанса. В базе он не сохранён, потому что отдельной таблицы ПСВ нет.";
+ return;
+ }
+
+ await LoadSelectedDocumentCoreAsync();
+ }
+
+ private async Task LoadSelectedDocumentCoreAsync()
+ {
+ try
+ {
+ LineStatusText = "Загрузка строк документа...";
+
+ var previousGroup = SelectedDocumentGroup;
+ var documentNumber = SelectedDocument.DocumentNumber;
+ var lines = await Task.Run(delegate { return _service.LoadDocumentLines(documentNumber); });
+
+ ClearCollections(DocumentLines);
+ foreach (var line in lines)
+ {
+ DocumentLines.Add(line);
+ }
+
+ RebuildDocumentGroupSummaries(lines);
+ SelectedDocumentGroup = FindMatchingGroup(previousGroup) ?? DocumentGroupSummaries.FirstOrDefault();
+ RefreshDocumentLinesView();
+ }
+ catch (Exception ex)
+ {
+ _dialogService.ShowError(ex.Message);
+ }
+ }
+
+ private void RaiseCommandStates()
+ {
+ ((RelayCommand)AddDocumentCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)DeleteDocumentCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)RefreshDocumentsCommand).RaiseCanExecuteChanged();
+ ((RelayCommand)SaveDocumentHeaderCommand).RaiseCanExecuteChanged();
+ }
+
+ private void RefreshDocumentLinesView()
+ {
+ DocumentLinesView.Refresh();
+ UpdateLineStatus();
+ }
+
+ private void RefreshDocumentsAsync(string documentToSelect)
+ {
+ ExecuteBusyOperationAsync(delegate { return RefreshDocumentsCoreAsync(documentToSelect); });
+ }
+
+ private async Task RefreshDocumentsCoreAsync(string documentToSelect)
+ {
+ DocumentStatusText = "Загрузка списка ПСВ...";
+
+ var databaseDocuments = await Task.Run(delegate { return _service.LoadDocuments(); });
+ var selectedDocumentNumber = documentToSelect ?? (SelectedDocument != null ? SelectedDocument.DocumentNumber : null);
+
+ ClearCollections(Documents);
+
+ foreach (var draft in _draftDocuments.OrderByDescending(delegate(PsvDocumentSummary item)
+ {
+ return item.AcceptedOn ?? DateTime.MinValue;
+ }))
+ {
+ Documents.Add(draft);
+ }
+
+ foreach (var document in databaseDocuments)
+ {
+ if (_draftDocuments.Any(delegate(PsvDocumentSummary draft)
+ {
+ return string.Equals(draft.DocumentNumber, document.DocumentNumber, StringComparison.OrdinalIgnoreCase);
+ }))
+ {
+ continue;
+ }
+
+ Documents.Add(document);
+ }
+
+ DocumentsView.Refresh();
+
+ SelectedDocument = Documents.FirstOrDefault(delegate(PsvDocumentSummary document)
+ {
+ return string.Equals(document.DocumentNumber, selectedDocumentNumber, StringComparison.OrdinalIgnoreCase);
+ });
+
+ if (SelectedDocument == null && Documents.Count > 0)
+ {
+ SelectedDocument = Documents[0];
+ }
+
+ DocumentStatusText = string.Format("Документов: {0}.", Documents.Count);
+ }
+
+ private void RebuildDocumentGroupSummaries(IEnumerable lines)
+ {
+ ClearCollections(DocumentGroupSummaries);
+
+ var groups = lines.GroupBy(line => new
+ {
+ InstrumentType = line.InstrumentType ?? string.Empty,
+ RangeText = line.RangeText ?? string.Empty,
+ RegistryNumber = line.RegistryNumber ?? string.Empty
+ })
+ .OrderBy(group => group.Key.InstrumentType)
+ .ThenBy(group => group.Key.RegistryNumber)
+ .ThenBy(group => group.Key.RangeText)
+ .Select(group => new PsvDocumentGroupSummary
+ {
+ InstrumentType = group.Key.InstrumentType,
+ RangeText = group.Key.RangeText,
+ RegistryNumber = group.Key.RegistryNumber,
+ InVerificationCount = group.Count(line => !line.IsPassed.HasValue),
+ VerifiedCount = group.Count(line => line.IsPassed.HasValue),
+ GoodCount = group.Count(line => line.IsPassed == true),
+ RejectedCount = group.Count(line => line.IsPassed == false)
+ });
+
+ foreach (var group in groups)
+ {
+ DocumentGroupSummaries.Add(group);
+ }
+ }
+
+ private void SaveDocumentHeaderAsync()
+ {
+ ExecuteBusyOperationAsync(async delegate
+ {
+ if (SelectedDocument == null)
+ {
+ _dialogService.ShowWarning("Сначала выберите документ.");
+ return;
+ }
+
+ if (SelectedDocument.IsDraft)
+ {
+ _dialogService.ShowWarning("Черновик нельзя сохранить в базе без строк EKZMK.");
+ return;
+ }
+
+ if (!HeaderReceivedOn.HasValue)
+ {
+ _dialogService.ShowWarning("Укажите дату приемки.");
+ return;
+ }
+
+ var request = new DocumentEditorResult
+ {
+ DocumentNumber = DocumentNumberEditor,
+ AcceptedOn = HeaderReceivedOn.Value,
+ IssuedOn = HeaderIssuedOn
+ };
+
+ await Task.Run(delegate { _service.UpdateDocumentHeader(SelectedDocument.DocumentNumber, request); });
+ await RefreshDocumentsCoreAsync(request.DocumentNumber);
+ _dialogService.ShowInfo("Реквизиты ПСВ обновлены.");
+ });
+ }
+
+ private void UpdateLineStatus()
+ {
+ if (SelectedDocument == null)
+ {
+ LineStatusText = "Документ не выбран.";
+ return;
+ }
+
+ if (SelectedDocument.IsDraft)
+ {
+ LineStatusText = "Черновик текущего сеанса. Строки появятся только после создания записей EKZMK.";
+ return;
+ }
+
+ if (DocumentGroupSummaries.Count == 0)
+ {
+ LineStatusText = "В документе нет групп приборов.";
+ return;
+ }
+
+ if (SelectedDocumentGroup == null)
+ {
+ LineStatusText = string.Format("Групп: {0}. Выберите группу.", DocumentGroupSummaries.Count);
+ return;
+ }
+
+ var groupLineCount = DocumentLines.Count(delegate(PsvDocumentLine line) { return SelectedDocumentGroup.Matches(line); });
+ var filteredCount = DocumentLines.Count(delegate(PsvDocumentLine line)
+ {
+ return SelectedDocumentGroup.Matches(line)
+ && (string.IsNullOrWhiteSpace(GroupDetailFilterText) || Contains(line.SerialNumber, GroupDetailFilterText));
+ });
+
+ LineStatusText = string.Format(
+ "Групп: {0}. Приборов в выбранной группе: {1}. Отображено по фильтру: {2}.",
+ DocumentGroupSummaries.Count,
+ groupLineCount,
+ filteredCount);
+ }
+
+ private static bool Contains(string source, string filter)
+ {
+ return !string.IsNullOrWhiteSpace(source)
+ && source.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+ }
+}
diff --git a/XLAB/MvvmInfrastructure.cs b/XLAB/MvvmInfrastructure.cs
new file mode 100644
index 0000000..9f1a537
--- /dev/null
+++ b/XLAB/MvvmInfrastructure.cs
@@ -0,0 +1,71 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Input;
+
+namespace XLAB
+{
+ public abstract class ObservableObject : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (Equals(field, value))
+ {
+ return false;
+ }
+
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ var handler = PropertyChanged;
+ if (handler != null)
+ {
+ handler(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+ }
+
+ internal sealed class RelayCommand : ICommand
+ {
+ private readonly Action