Применение к сессии, USE заменён на checkbox
This commit is contained in:
10
README.md
10
README.md
@@ -71,14 +71,14 @@ dotnet run --project src/Sms.TaskTwo.Avalonia/Sms.TaskTwo.Avalonia.csproj
|
||||
|
||||
### Windows
|
||||
|
||||
- Чтение/запись: `EnvironmentVariableTarget.User` (реестр `HKEY_CURRENT_USER\Environment`).
|
||||
- После записи отправляется `WM_SETTINGCHANGE`, чтобы обновить env в уже запущенных GUI-приложениях.
|
||||
- Чтение/запись: реестр `HKEY_CURRENT_USER\Environment`.
|
||||
- **Применить к сессии** (`ReloadEnvironment`): обновляет env текущего процесса и рассылает `WM_SETTINGCHANGE` для других GUI-приложений.
|
||||
|
||||
### Linux
|
||||
|
||||
- Запись в `~/.config/environment.d/99-sms-task-two.conf` (формат systemd `KEY=value`, значения с пробелами в кавычках).
|
||||
- Чтение: merge всех `*.conf` в `environment.d`, затем fallback на env текущего процесса.
|
||||
- Для новых login-сессий может потребоваться перелогин или `systemctl --user import-environment` — ограничение systemd, не ошибка приложения.
|
||||
- Запись в `~/.config/environment.d/` (systemd `KEY=value`).
|
||||
- **Применить к сессии** (`ReloadEnvironment`): текущий процесс, `systemctl --user set-environment`, при наличии — `dbus-update-environment`.
|
||||
- Новые login-сессии могут потребовать перелогин — ограничение systemd.
|
||||
|
||||
## Предположения (ТЗ)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using Sms.Environment;
|
||||
|
||||
namespace Sms.Environment.Linux;
|
||||
@@ -5,6 +6,7 @@ namespace Sms.Environment.Linux;
|
||||
public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
{
|
||||
private const string ManagedFileName = "99-sms-task-two.conf";
|
||||
private const int SystemctlBatchSize = 32;
|
||||
|
||||
private readonly string _managedFilePath;
|
||||
private readonly string _environmentDirectory;
|
||||
@@ -119,6 +121,7 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
}
|
||||
|
||||
System.Environment.SetEnvironmentVariable(name, null);
|
||||
TryRunSystemctl(["--user", "unset-environment", name], out _);
|
||||
}
|
||||
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
|
||||
{
|
||||
@@ -128,6 +131,142 @@ public sealed class LinuxEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
}
|
||||
}
|
||||
|
||||
public EnvironmentReloadResult ReloadEnvironment()
|
||||
{
|
||||
var variables = GetUserPersistedEnvironment();
|
||||
ApplyToCurrentProcess(variables);
|
||||
|
||||
if (variables.Count == 0)
|
||||
{
|
||||
return new EnvironmentReloadResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "Пользовательских переменных для применения нет.",
|
||||
};
|
||||
}
|
||||
|
||||
var messages = new List<string>();
|
||||
if (TryApplyViaSystemctl(variables, out var systemctlError))
|
||||
{
|
||||
messages.Add("systemd user manager обновлён (systemctl --user set-environment).");
|
||||
}
|
||||
else if (systemctlError is not null)
|
||||
{
|
||||
messages.Add($"systemctl: {systemctlError}");
|
||||
}
|
||||
|
||||
if (TryApplyViaDbusUpdateEnvironment(variables, out var dbusError))
|
||||
{
|
||||
messages.Add("D-Bus session обновлён (dbus-update-environment).");
|
||||
}
|
||||
else if (dbusError is not null)
|
||||
{
|
||||
messages.Add($"dbus-update-environment: {dbusError}");
|
||||
}
|
||||
|
||||
var processApplied = messages.Count > 0 || variables.Count > 0;
|
||||
return new EnvironmentReloadResult
|
||||
{
|
||||
Success = processApplied,
|
||||
Message = processApplied
|
||||
? $"Применено {variables.Count} переменных к текущему процессу. {string.Join(' ', messages)}"
|
||||
: "Не удалось применить переменные к сессии.",
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyToCurrentProcess(IReadOnlyDictionary<string, string> variables)
|
||||
{
|
||||
foreach (var pair in variables)
|
||||
{
|
||||
System.Environment.SetEnvironmentVariable(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyViaSystemctl(
|
||||
IReadOnlyDictionary<string, string> variables,
|
||||
out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
var pairs = variables.ToList();
|
||||
for (var offset = 0; offset < pairs.Count; offset += SystemctlBatchSize)
|
||||
{
|
||||
var arguments = new List<string> { "--user", "set-environment" };
|
||||
foreach (var pair in pairs.Skip(offset).Take(SystemctlBatchSize))
|
||||
{
|
||||
arguments.Add($"{pair.Key}={pair.Value}");
|
||||
}
|
||||
|
||||
if (!TryRunSystemctl(arguments, out errorMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryApplyViaDbusUpdateEnvironment(
|
||||
IReadOnlyDictionary<string, string> variables,
|
||||
out string? errorMessage)
|
||||
{
|
||||
var arguments = new List<string>();
|
||||
foreach (var pair in variables)
|
||||
{
|
||||
arguments.Add($"{pair.Key}={pair.Value}");
|
||||
}
|
||||
|
||||
return TryRunCommand("dbus-update-environment", arguments, out errorMessage);
|
||||
}
|
||||
|
||||
private static bool TryRunSystemctl(IReadOnlyList<string> arguments, out string? errorMessage) =>
|
||||
TryRunCommand("systemctl", arguments, out errorMessage);
|
||||
|
||||
private static bool TryRunCommand(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
errorMessage = "не удалось запустить процесс";
|
||||
return false;
|
||||
}
|
||||
|
||||
var stderr = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit(5000);
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = string.IsNullOrWhiteSpace(stderr)
|
||||
? $"код выхода {process.ExitCode}"
|
||||
: stderr.Trim();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> LoadManagedFile()
|
||||
{
|
||||
if (!File.Exists(_managedFilePath))
|
||||
|
||||
@@ -83,6 +83,25 @@ public sealed class WindowsEnvironmentVariableStore : IEnvironmentVariableStore
|
||||
BroadcastEnvironmentChange();
|
||||
}
|
||||
|
||||
public EnvironmentReloadResult ReloadEnvironment()
|
||||
{
|
||||
var variables = GetUserPersistedEnvironment();
|
||||
foreach (var pair in variables)
|
||||
{
|
||||
System.Environment.SetEnvironmentVariable(pair.Key, pair.Value, EnvironmentVariableTarget.Process);
|
||||
}
|
||||
|
||||
BroadcastEnvironmentChange();
|
||||
|
||||
return new EnvironmentReloadResult
|
||||
{
|
||||
Success = true,
|
||||
Message = variables.Count == 0
|
||||
? "Пользовательских переменных для применения нет."
|
||||
: $"Применено {variables.Count} переменных к текущему процессу и отправлено WM_SETTINGCHANGE для других приложений.",
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ToDictionary(System.Collections.IDictionary source)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
8
src/Sms.Environment/EnvironmentReloadResult.cs
Normal file
8
src/Sms.Environment/EnvironmentReloadResult.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Sms.Environment;
|
||||
|
||||
public sealed class EnvironmentReloadResult
|
||||
{
|
||||
public required bool Success { get; init; }
|
||||
|
||||
public required string Message { get; init; }
|
||||
}
|
||||
@@ -19,4 +19,6 @@ public interface IEnvironmentVariableStore
|
||||
bool IsPersistedInUserStore(string name);
|
||||
|
||||
void RemoveFromUserStore(string name);
|
||||
|
||||
EnvironmentReloadResult ReloadEnvironment();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
Margin="12,8,12,0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnDefinitions="*,Auto,Auto"
|
||||
ColumnSpacing="8">
|
||||
<CheckBox Content="Отображать все переменные"
|
||||
IsChecked="{Binding ShowAllVariables}"
|
||||
@@ -52,6 +52,10 @@
|
||||
Content="Обновить"
|
||||
Command="{Binding RefreshCommand}"
|
||||
MinWidth="100" />
|
||||
<Button Grid.Column="2"
|
||||
Content="Применить к сессии"
|
||||
Command="{Binding ReloadEnvironmentCommand}"
|
||||
MinWidth="140" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2"
|
||||
@@ -73,11 +77,17 @@
|
||||
MinWidth="100" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="3"
|
||||
<StackPanel Grid.Row="3"
|
||||
Margin="12,4,12,0"
|
||||
Foreground="#C62828"
|
||||
Spacing="2">
|
||||
<TextBlock Foreground="#C62828"
|
||||
Text="{Binding AddVariableError}"
|
||||
IsVisible="{Binding AddVariableError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<TextBlock Foreground="#2E7D32"
|
||||
Text="{Binding ReloadEnvironmentMessage}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding ReloadEnvironmentMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid x:Name="VariablesGrid"
|
||||
Grid.Row="4"
|
||||
@@ -93,10 +103,17 @@
|
||||
BorderBrush="#B0B0B0"
|
||||
LoadingRow="OnLoadingRow">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header=""
|
||||
Binding="{Binding UserStoreBadge}"
|
||||
IsReadOnly="True"
|
||||
Width="56" />
|
||||
<DataGridTemplateColumn Header=""
|
||||
Width="44">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:EnvironmentVariableRowViewModel">
|
||||
<CheckBox IsChecked="{Binding UseUserStore, Mode=TwoWay}"
|
||||
IsEnabled="{Binding CanChangeUserStore}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Поле"
|
||||
Binding="{Binding Field}"
|
||||
IsReadOnly="True"
|
||||
@@ -115,17 +132,6 @@
|
||||
Binding="{Binding Comment, Mode=TwoWay}"
|
||||
Width="240"
|
||||
MinWidth="180" />
|
||||
<DataGridTemplateColumn Header=""
|
||||
Width="80">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="vm:EnvironmentVariableRowViewModel">
|
||||
<Button Content="Удалить"
|
||||
Command="{Binding DeleteFromUserStoreCommand}"
|
||||
Padding="6,2"
|
||||
FontSize="11" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
@@ -87,7 +87,7 @@ public partial class MainWindow : Window
|
||||
gridRow.Classes.Add("custom");
|
||||
}
|
||||
|
||||
if (row.IsPersistedInUserStore)
|
||||
if (row.UseUserStore)
|
||||
{
|
||||
gridRow.Classes.Add("userStore");
|
||||
}
|
||||
|
||||
@@ -145,6 +145,14 @@ public sealed class EnvironmentVariablesService
|
||||
public string? GetProcessValue(string name) =>
|
||||
_store.GetProcessEnvironment().TryGetValue(name, out var value) ? value : null;
|
||||
|
||||
public EnvironmentReloadResult ReloadEnvironment()
|
||||
{
|
||||
var result = _store.ReloadEnvironment();
|
||||
_log.WriteLine(
|
||||
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] Reload environment: success={result.Success}; {result.Message}");
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SaveValue(string name, string value)
|
||||
{
|
||||
var previous = _store.GetUserPersistedValue(name);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Sms.TaskTwo.Core.Models;
|
||||
using Sms.TaskTwo.Core.Services;
|
||||
|
||||
@@ -8,14 +7,19 @@ namespace Sms.TaskTwo.ViewModels;
|
||||
public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
{
|
||||
private readonly EnvironmentVariablesService _service;
|
||||
private readonly Action? _onCustomRemoved;
|
||||
private bool _isLoading;
|
||||
|
||||
public EnvironmentVariableRowViewModel(EnvironmentVariableRow row, EnvironmentVariablesService service)
|
||||
public EnvironmentVariableRowViewModel(
|
||||
EnvironmentVariableRow row,
|
||||
EnvironmentVariablesService service,
|
||||
Action? onCustomRemoved = null)
|
||||
{
|
||||
Field = row.Field;
|
||||
IsFromAppSettings = row.IsFromAppSettings;
|
||||
IsCustom = row.IsCustom;
|
||||
_service = service;
|
||||
_onCustomRemoved = onCustomRemoved;
|
||||
ApplySnapshot(row, suppressSave: true);
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
@@ -26,8 +30,10 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
public bool IsCustom { get; }
|
||||
|
||||
public bool CanChangeUserStore => !IsFromAppSettings;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isPersistedInUserStore;
|
||||
private bool _useUserStore;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _actualValue = string.Empty;
|
||||
@@ -35,36 +41,60 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
public string ActualValueDisplay =>
|
||||
string.IsNullOrEmpty(ActualValue) ? "<нет>" : ActualValue;
|
||||
|
||||
public string UserStoreBadge => IsPersistedInUserStore ? "USER" : string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _requiredValue = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _comment = string.Empty;
|
||||
|
||||
partial void OnIsPersistedInUserStoreChanged(bool value)
|
||||
partial void OnUseUserStoreChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(UserStoreBadge));
|
||||
DeleteFromUserStoreCommand.NotifyCanExecuteChanged();
|
||||
RowAppearanceChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFromAppSettings && !value)
|
||||
{
|
||||
BeginLoad();
|
||||
UseUserStore = true;
|
||||
EndLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
_service.SaveValue(Field, RequiredValue);
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsCustom)
|
||||
{
|
||||
_service.RemoveVariable(Field);
|
||||
_onCustomRemoved?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_service.DeleteFromUserStore(Field);
|
||||
BeginLoad();
|
||||
RequiredValue = _service.GetDisplayValue(Field);
|
||||
EndLoad();
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
|
||||
partial void OnActualValueChanged(string value) => OnPropertyChanged(nameof(ActualValueDisplay));
|
||||
|
||||
partial void OnRequiredValueChanged(string value)
|
||||
{
|
||||
if (_isLoading)
|
||||
if (_isLoading || !UseUserStore)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_service.SaveValue(Field, value);
|
||||
if (!IsPersistedInUserStore)
|
||||
{
|
||||
IsPersistedInUserStore = true;
|
||||
}
|
||||
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
|
||||
@@ -79,7 +109,7 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
RequiredValue = row.Value;
|
||||
Comment = row.Comment;
|
||||
IsPersistedInUserStore = row.IsPersistedInUserStore;
|
||||
UseUserStore = row.IsFromAppSettings || row.IsPersistedInUserStore;
|
||||
|
||||
if (suppressSave)
|
||||
{
|
||||
@@ -105,26 +135,4 @@ public sealed partial class EnvironmentVariableRowViewModel : ObservableObject
|
||||
|
||||
_service.SaveComment(Field, value);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanDeleteFromUserStore))]
|
||||
private void DeleteFromUserStore()
|
||||
{
|
||||
if (IsCustom)
|
||||
{
|
||||
_service.RemoveVariable(Field);
|
||||
Removed?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
_service.DeleteFromUserStore(Field);
|
||||
BeginLoad();
|
||||
IsPersistedInUserStore = false;
|
||||
RequiredValue = _service.GetDisplayValue(Field);
|
||||
EndLoad();
|
||||
RefreshActualValue(_service.GetProcessValue(Field));
|
||||
}
|
||||
|
||||
public event EventHandler? Removed;
|
||||
|
||||
private bool CanDeleteFromUserStore() => IsPersistedInUserStore || IsCustom;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Sms.TaskTwo.Core.Models;
|
||||
using Sms.TaskTwo.Core.Services;
|
||||
|
||||
namespace Sms.TaskTwo.ViewModels;
|
||||
@@ -31,6 +32,9 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string? _addVariableError;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _reloadEnvironmentMessage;
|
||||
|
||||
partial void OnShowAllVariablesChanged(bool value) => SyncRowsFromService();
|
||||
|
||||
partial void OnNewVariableNameChanged(string value) => AddVariableCommand.NotifyCanExecuteChanged();
|
||||
@@ -42,6 +46,14 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
RefreshProcessStates();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ReloadEnvironment()
|
||||
{
|
||||
var result = _service.ReloadEnvironment();
|
||||
ReloadEnvironmentMessage = result.Message;
|
||||
RefreshProcessStates();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanAddVariable))]
|
||||
private void AddVariable()
|
||||
{
|
||||
@@ -57,18 +69,23 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
NewVariableValue = string.Empty;
|
||||
|
||||
var row = _service.GetRowSnapshot(name);
|
||||
var viewModel = new EnvironmentVariableRowViewModel(row, _service);
|
||||
viewModel.Removed += OnRowRemoved;
|
||||
Rows.Insert(FindInsertIndexForCustomVariable(), viewModel);
|
||||
Rows.Insert(FindInsertIndexForCustomVariable(), CreateRowViewModel(row));
|
||||
}
|
||||
|
||||
private void OnRowRemoved(object? sender, EventArgs e)
|
||||
private EnvironmentVariableRowViewModel CreateRowViewModel(EnvironmentVariableRow row)
|
||||
{
|
||||
if (sender is EnvironmentVariableRowViewModel row)
|
||||
EnvironmentVariableRowViewModel? viewModel = null;
|
||||
viewModel = new EnvironmentVariableRowViewModel(
|
||||
row,
|
||||
_service,
|
||||
onCustomRemoved: () =>
|
||||
{
|
||||
row.Removed -= OnRowRemoved;
|
||||
Rows.Remove(row);
|
||||
if (viewModel is not null)
|
||||
{
|
||||
Rows.Remove(viewModel);
|
||||
}
|
||||
});
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private int FindInsertIndexForCustomVariable()
|
||||
@@ -93,10 +110,8 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
for (var index = Rows.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var row = Rows[index];
|
||||
if (data.All(item => item.Field != row.Field))
|
||||
if (data.All(item => item.Field != Rows[index].Field))
|
||||
{
|
||||
row.Removed -= OnRowRemoved;
|
||||
Rows.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
@@ -113,8 +128,7 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel = new EnvironmentVariableRowViewModel(row, _service);
|
||||
viewModel.Removed += OnRowRemoved;
|
||||
viewModel = CreateRowViewModel(row);
|
||||
existingByField[row.Field] = viewModel;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user