You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
22 KiB
547 lines
22 KiB
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows;
|
|
using System.Windows.Input;
|
|
using System.Windows.Threading;
|
|
using SmartAquaViewer.Controls;
|
|
using SmartAquaViewer.DataAnalysis;
|
|
using SmartAquaViewer.Model;
|
|
|
|
namespace SmartAquaViewer.ViewModel
|
|
{
|
|
/// <summary>
|
|
/// 데이터에서 추출한 필드 정보
|
|
/// </summary>
|
|
public sealed class FieldItem
|
|
{
|
|
public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue")
|
|
public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)")
|
|
public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등
|
|
public StepFieldKind Kind { get; init; }
|
|
|
|
public override string? ToString() => Display;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 시점에 기록된 수조 데이터 묶음
|
|
/// </summary>
|
|
public class TanksByTime
|
|
{
|
|
public DateTime RecordedTime { get; }
|
|
public List<WaterTank> Tanks { get; } = new();
|
|
|
|
public TanksByTime(DateTime time, List<WaterTank> tanks)
|
|
{
|
|
RecordedTime = time;
|
|
Tanks = tanks;
|
|
}
|
|
}
|
|
|
|
public class MonitoringViewModel : PagingViewModelBase<WaterQualityVO>
|
|
{
|
|
#region Properties
|
|
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
|
|
|
|
public ObservableCollection<GraphType> GraphTypes { get; }
|
|
|
|
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; }
|
|
|
|
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; set; }
|
|
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
|
|
|
|
public ObservableCollection<TanksByTime> TanksByTimes { get; } = new();
|
|
public PagingViewModelBase<TanksByTime> TanksPager { get; } = new();
|
|
|
|
|
|
private MonitorTab _selectedTab;
|
|
public MonitorTab SelectedTab
|
|
{
|
|
get => _selectedTab;
|
|
set
|
|
{
|
|
if (_selectedTab != value)
|
|
{
|
|
_selectedTab = value;
|
|
OnPropertyChanged();
|
|
|
|
RebuildAvailableFields(); // 탭에 맞춰 필드 목록 재구성
|
|
SetGraphType();
|
|
OnPropertyChanged(nameof(IsTankAndLine));
|
|
|
|
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
RebuildFieldCandidates();
|
|
}), DispatcherPriority.Background);
|
|
|
|
OnSystemChanged?.Invoke(SelectedTab);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int _selectedGraphIndex = -1;
|
|
public int SelectedGraphIndex
|
|
{
|
|
get => _selectedGraphIndex;
|
|
set
|
|
{
|
|
if (_selectedGraphIndex != value)
|
|
{
|
|
_selectedGraphIndex = value;
|
|
OnPropertyChanged();
|
|
// 인덱스가 바뀌면 enum도 맞춰준다
|
|
if (value >= 0 && value < GraphTypes.Count)
|
|
SelectedGraphType = GraphTypes[value];
|
|
}
|
|
}
|
|
}
|
|
|
|
private GraphType _selectedGraphType;
|
|
public GraphType SelectedGraphType
|
|
{
|
|
get => _selectedGraphType;
|
|
set
|
|
{
|
|
if (_selectedGraphType != value)
|
|
{
|
|
_selectedGraphType = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(ShowXSelector));
|
|
RebuildFieldCandidates();
|
|
|
|
var idx = GraphTypes.IndexOf(value);
|
|
if (idx != -1 && idx != _selectedGraphIndex)
|
|
{
|
|
_selectedGraphIndex = idx;
|
|
OnPropertyChanged(nameof(SelectedGraphIndex));
|
|
OnPropertyChanged(nameof(IsTankAndLine));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private StepFieldKind _selectedKind = StepFieldKind.Sensor; // 기본값은 센서
|
|
public StepFieldKind SelectedKind
|
|
{
|
|
get => _selectedKind;
|
|
set
|
|
{
|
|
if (_selectedKind != value)
|
|
{
|
|
_selectedKind = value;
|
|
OnPropertyChanged();
|
|
// 라디오 변경 시 Y 후보 재구성
|
|
RebuildFieldCandidates();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsTankAndLine
|
|
{
|
|
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
|
|
}
|
|
|
|
private bool _isOpenMode;
|
|
public bool IsOpenMode
|
|
{
|
|
get => _isOpenMode;
|
|
set
|
|
{
|
|
if (_isOpenMode != value)
|
|
{
|
|
_isOpenMode = value;
|
|
OnPropertyChanged();
|
|
|
|
BtnVisibilityDown = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
|
|
BtnVisibilityUp = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Visibility _btnVisibilityDown;
|
|
public Visibility BtnVisibilityDown
|
|
{
|
|
get => _btnVisibilityDown;
|
|
set
|
|
{
|
|
if (_btnVisibilityDown != value)
|
|
{
|
|
_btnVisibilityDown = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private Visibility _btnVisibilityUp;
|
|
public Visibility BtnVisibilityUp
|
|
{
|
|
get => _btnVisibilityUp;
|
|
set
|
|
{
|
|
if (_btnVisibilityUp != value)
|
|
{
|
|
_btnVisibilityUp = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowXSelector => SelectedGraphType == GraphType.SCATTER;
|
|
|
|
// [필드 후보 목록] 탭/시스템에 따라 달라짐
|
|
public ObservableCollection<FieldItem> AvailableFields { get; } = new();
|
|
|
|
// [X축 후보/선택]
|
|
public ObservableCollection<FieldItem> XFieldCandidates { get; } = new();
|
|
private FieldItem? _selectedXField;
|
|
public FieldItem? SelectedXField
|
|
{
|
|
get => _selectedXField;
|
|
set { if (_selectedXField != value) { _selectedXField = value; OnPropertyChanged(); } }
|
|
}
|
|
|
|
// [Y축 후보/선택] — Line/Step: 다중, Scatter/Box: 단일
|
|
public ObservableCollection<FieldItem> YFieldCandidates { get; } = new();
|
|
|
|
// 다중 선택(Y)용
|
|
public ObservableCollection<FieldItem> SelectedYFields { get; } = new();
|
|
|
|
// 단일 선택(Y)용
|
|
private FieldItem? _selectedYField;
|
|
public FieldItem? SelectedYField
|
|
{
|
|
get => _selectedYField;
|
|
set { if (_selectedYField != value) { _selectedYField = value; OnPropertyChanged(); } }
|
|
}
|
|
|
|
// [옵션] 예시 — 필요하면 추가
|
|
private bool _showMarkers; // Line
|
|
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
|
|
|
|
private bool _showLegends;
|
|
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
|
|
|
|
private bool _useSmoothing; // Line
|
|
public bool UseSmoothing { get => _useSmoothing; set { _useSmoothing = value; OnPropertyChanged(); } }
|
|
|
|
private double _scatterMarkerSize = 3; // Scatter
|
|
public double ScatterMarkerSize { get => _scatterMarkerSize; set { _scatterMarkerSize = value; OnPropertyChanged(); } }
|
|
|
|
private bool _showRegression; // Scatter
|
|
public bool ShowRegression { get => _showRegression; set { _showRegression = value; OnPropertyChanged(); } }
|
|
|
|
private double _boxWidth = 0.3; // Box
|
|
public double BoxWidth { get => _boxWidth; set { _boxWidth = value; OnPropertyChanged(); } }
|
|
|
|
private int _boxTimeSpan = 6; // Box
|
|
public int BoxTimeSpan
|
|
{
|
|
get => _boxTimeSpan;
|
|
set
|
|
{
|
|
if (_boxTimeSpan != value)
|
|
{
|
|
_boxTimeSpan = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
public ICommand ChangeDrawerStatusCommand { get; }
|
|
public ICommand DrawGraphCommand { get; }
|
|
|
|
public delegate void SystemChangedEventHandler(MonitorTab selectedTab);
|
|
public event SystemChangedEventHandler OnSystemChanged;
|
|
|
|
|
|
|
|
public MonitoringViewModel()
|
|
{
|
|
IsOpenMode = true;
|
|
BtnVisibilityUp = Visibility.Collapsed;
|
|
|
|
WaterQualityList = Datas.Instance.WaterQualityView;
|
|
|
|
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnWaterQualityChanged;
|
|
|
|
RebuildAllGroups();
|
|
|
|
GraphTypes = [];
|
|
SelectedTab = MonitorTab.Tank; // Default system
|
|
SetGraphType();
|
|
|
|
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
|
|
DrawGraphCommand = new RelayCommand(DrawGraph);
|
|
|
|
RebuildAvailableFields();
|
|
RebuildFieldCandidates();
|
|
}
|
|
|
|
private void OnWaterQualityChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildAllGroups();
|
|
|
|
|
|
private void RebuildAllGroups()
|
|
{
|
|
var grouped = WaterQualityList
|
|
.GroupBy(x => x.Tank.Number) // 또는 x.Tank.Num
|
|
.OrderBy(g => g.Key)
|
|
.ToDictionary(
|
|
g => g.Key,
|
|
g => new ObservableCollection<WaterQualityVO>(
|
|
g.OrderBy(r => r.RecordedTime))
|
|
);
|
|
|
|
TankGroups = grouped;
|
|
OnPropertyChanged(nameof(TankGroups));
|
|
|
|
TanksPager.Items.Clear();
|
|
foreach (var w in WaterQualityList)
|
|
TanksPager.Items.Add(new TanksByTime(w.RecordedTime, w.Tanks));
|
|
|
|
var ordered = TanksPager.Items.OrderBy(t => t.RecordedTime).ToList();
|
|
TanksPager.Items.Clear();
|
|
foreach (var t in ordered) TanksPager.Items.Add(t);
|
|
|
|
// TankGroups를 깔끔하게 다시 구성
|
|
this.Items.Clear();
|
|
foreach (var w in WaterQualityList)
|
|
this.Items.Add(w);
|
|
}
|
|
|
|
private void DrawGraph(object obj)
|
|
{
|
|
switch (SelectedGraphType)
|
|
{
|
|
case GraphType.LINE:
|
|
if (SelectedTab.Equals(MonitorTab.Tank))
|
|
SetGraphData_Line_Tank();
|
|
else
|
|
GraphControlVM.SetDefaultLineGraph(
|
|
WaterQualityList.ToList(), SelectedTab, SelectedXField, SelectedYField, ShowMarkers);
|
|
break;
|
|
case GraphType.BOX:
|
|
var xFieldBox = SelectedXField;
|
|
SetGraphData_Box_Tank();
|
|
break;
|
|
case GraphType.SCATTER:
|
|
SetGraphData_Scatter_Tank();
|
|
break;
|
|
case GraphType.STEP:
|
|
var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
|
|
var tFieldsStep = SelectedYFields;
|
|
var yFiledStep = SelectedYField;
|
|
var showMarkerStep = ShowMarkers;
|
|
if (SelectedKind.Equals(StepFieldKind.Status))
|
|
GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList.ToList(), SelectedYField, SelectedYFields, ShowMarkers, ShowLegends);
|
|
else
|
|
GraphControlVM.SetStepPlot(WaterQualityList.ToList(), SelectedTab, xFieldStep, SelectedYFields, ShowMarkers, ShowLegends);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void SetGraphData_Line_Tank()
|
|
{
|
|
if (SelectedTab != MonitorTab.Tank) return;
|
|
if (SelectedYField == null) return;
|
|
|
|
var keys = SelectedWaterTanks.Keys.ToList();
|
|
|
|
GraphControlVM.SetTankLineGraph(WaterQualityList, keys, SelectedXField, SelectedYField, ShowMarkers, ShowLegends);
|
|
}
|
|
|
|
private void SetGraphData_Box_Tank()
|
|
{
|
|
if (SelectedTab != MonitorTab.Tank) return;
|
|
if (SelectedYField == null) return;
|
|
|
|
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
|
|
|
|
var keys = SelectedWaterTanks.Keys.ToList();
|
|
|
|
GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField!, SelectedYField, BoxWidth, boxTimeSpan);
|
|
}
|
|
|
|
private void SetGraphData_Scatter_Tank()
|
|
{
|
|
if (SelectedTab != MonitorTab.Tank) return;
|
|
if (SelectedYField == null) return;
|
|
|
|
var keys = SelectedWaterTanks.Keys.ToList();
|
|
|
|
GraphControlVM.SetScatterPlot(WaterQualityList, SelectedXField!, SelectedYField, keys, ScatterMarkerSize, ShowRegression, ShowLegends);
|
|
}
|
|
|
|
private void SetGraphType()
|
|
{
|
|
GraphTypes.Clear();
|
|
|
|
switch (SelectedTab)
|
|
{
|
|
case MonitorTab.Tank:
|
|
GraphTypes.Add(GraphType.LINE);
|
|
GraphTypes.Add(GraphType.BOX);
|
|
GraphTypes.Add(GraphType.SCATTER);
|
|
break;
|
|
case MonitorTab.Filter:
|
|
case MonitorTab.Sterilizer:
|
|
GraphTypes.Add(GraphType.LINE);
|
|
GraphTypes.Add(GraphType.STEP);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
SelectedGraphIndex = -1;
|
|
SelectedGraphIndex = GraphTypes.Count > 0 ? 0 : -1;
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RebuildAvailableFields()
|
|
{
|
|
AvailableFields.Clear();
|
|
|
|
// 공통 시간
|
|
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime), Kind = StepFieldKind.Time });
|
|
|
|
if (SelectedTab == MonitorTab.Tank)
|
|
{
|
|
AvailableFields.Add(new FieldItem { Name = "Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "DOValue", Display = "DO (mg/L)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "PH", Display = "pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "ORP", Display = "ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
}
|
|
else if (SelectedTab == MonitorTab.Filter)
|
|
{
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterPower", Display = "모래여과기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpPower", Display = "순환펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.InverterControllerStatus", Display = "인버터 제어기 상태", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpPower", Display = "히트펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerPower", Display = "에어브로와 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
}
|
|
else // Sterilizer
|
|
{
|
|
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
|
|
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorPower", Display = "오존 발생기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPower", Display = "오존용해장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerPower", Display = "배오존장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
|
|
AddUvPowerFieldsPerId(WaterQualityList);
|
|
}
|
|
}
|
|
|
|
// rows: 현재 그리려는 데이터(필터링/정렬 반영된)
|
|
private void AddUvPowerFieldsPerId(IEnumerable<WaterQualityVO> rows)
|
|
{
|
|
// 케이스 A: 한 행이 UV 한 대의 상태를 담는 스키마
|
|
var idsA = rows
|
|
.Select(r => r?.Sterilizing?.UVSterilizerId)
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
foreach (var id in idsA)
|
|
{
|
|
// Name 규칙: "Sterilizing.UVSterilizerPower[id=XXX]"
|
|
AvailableFields.Add(new FieldItem
|
|
{
|
|
Name = $"Sterilizing.UVSterilizerPower[id={id}]",
|
|
Display = $"자외선 살균기 {id} 전원",
|
|
DataType = typeof(int),
|
|
Kind = StepFieldKind.Status
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// 그래프 타입이 바뀔 때 후보/기본 선택 재구성
|
|
private void RebuildFieldCandidates()
|
|
{
|
|
// 후보 초기화
|
|
XFieldCandidates.Clear();
|
|
YFieldCandidates.Clear();
|
|
|
|
// X축: 시간 우선
|
|
foreach (var f in AvailableFields)
|
|
{
|
|
if ((SelectedGraphType == GraphType.LINE
|
|
|| SelectedGraphType == GraphType.STEP
|
|
|| SelectedGraphType == GraphType.SCATTER)
|
|
&& f.Name!.Equals("Number")) continue;
|
|
if (SelectedGraphType == GraphType.LINE
|
|
&& (f.Kind.Equals(StepFieldKind.Status)
|
|
|| f.Name!.Equals("Filtering.InverterControllerStatus"))) continue;
|
|
XFieldCandidates.Add(f);
|
|
if (SelectedGraphType == GraphType.STEP || SelectedGraphType == GraphType.BOX) break;
|
|
}
|
|
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
|
|
?? AvailableFields.FirstOrDefault();
|
|
|
|
if (SelectedGraphType != GraphType.STEP) SelectedKind = StepFieldKind.Sensor;
|
|
|
|
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
|
|
|
|
if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX)
|
|
{
|
|
// 수치형만 (LINE/SCATTER/BOX는 연속값 위주)
|
|
src = src.Where(f => f.DataType == typeof(double));
|
|
}
|
|
else if (SelectedGraphType == GraphType.STEP)
|
|
{
|
|
// STEP은 상태 전환에 잘 맞음: int/bool 위주
|
|
src = src.Where(f => f.DataType == typeof(int) || f.DataType == typeof(bool) || f.DataType == typeof(double));
|
|
// (상태가 double로 들어오는 경우도 있을 수 있어 double 허용)
|
|
}
|
|
|
|
// Y축 후보: 수치형
|
|
foreach (var f in src) YFieldCandidates.Add(f);
|
|
|
|
// 기본 선택 세팅 (타입별)
|
|
SelectedYFields.Clear();
|
|
SelectedYField = null;
|
|
|
|
switch (SelectedGraphType)
|
|
{
|
|
case GraphType.LINE:
|
|
SelectedYField = YFieldCandidates.FirstOrDefault();
|
|
ShowMarkers = false;
|
|
UseSmoothing = false;
|
|
break;
|
|
case GraphType.STEP:
|
|
ShowMarkers = false;
|
|
UseSmoothing = false;
|
|
break;
|
|
|
|
case GraphType.SCATTER:
|
|
SelectedYField = YFieldCandidates.FirstOrDefault();
|
|
ScatterMarkerSize = 3;
|
|
ShowRegression = false;
|
|
break;
|
|
|
|
case GraphType.BOX:
|
|
SelectedYField = YFieldCandidates.FirstOrDefault();
|
|
BoxWidth = 0.3;
|
|
break;
|
|
}
|
|
OnPropertyChanged(nameof(SelectedYFields));
|
|
}
|
|
|
|
protected new void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
=> base.OnPropertyChanged(name);
|
|
}
|
|
}
|