사용자 지적: "속성패널 디자인 보기 불편한데? 고도화해줘".
[실측한 낭비] 단일 선택 시 '공통+글꼴'만으로 354px 를 쓴다. 표준 행 34px 중 실제 필요 높이는
18px(테두리 2 + 글자 16)뿐이라 행마다 12px 가 공기다. 가장 긴 TextBox 는 25행 801px 로 기본 창
뷰포트(약 790px)를 이미 넘겨 스크롤이 강제됐다.
[쌍 배치] X|Y, 너비|높이를 한 줄 2칸으로. 라벨은 칩 바깥 왼쪽 자연폭 — [200] 의 XYPair 규약
(Grid star/10/star)과 같다. 네 줄이 두 줄이 된다.
구조가 핵심이다: **자식 행은 평범한 행 그대로**이고 컨테이너만 새로 만들었다. 값·커밋·"여러 값"
판정이 전부 자식 안에서 끝나므로 커밋 규약(Initialize 는 Commit 을 내지 않고, 값이 바뀔 때만
Undo 스냅샷 1회)이 한 글자도 안 바뀌고, boundsRows 가 자식을 들고 있어 드래그 중 실시간 갱신
(RefreshBoundsRows)도 그대로다. X 만 갈리고 Y 는 같은 다중선택도 칸별로 옳게 표시된다
(그래서 셀마다 전용 Grid 를 둔다 — 오버레이의 포커스 트리거가 AncestorType=Grid 다).
[스타일 세그먼트] 굵게·기울임·밑줄·취소선을 한 줄 아이콘 토글로. 종전에는 굵게·밑줄만 있었고
각각 한 줄이었다. **기울임·취소선은 기술적 제약이 아니라 단순 누락이었다** — LegacyFont 의
파싱·직렬화·렌더·폰트 일괄 변경 대화상자까지 전부 구현돼 있고 인스펙터만 호출을 빠뜨렸다.
CurrentFontOf 가 Italic/Strikeout 을 복사·보존하므로 네 개를 동시에 노출해도 왕복이 안 깨진다.
[행 치수] 인스펙터 전용 지역 스타일로 입력 높이 30→26. 전역 TextBox 를 낮추면 검색칩·대화상자
입력까지 따라 줄어 버튼과 정렬이 깨진다. **ComboBox 는 MinHeight 가 아니라 Height 고정**이라
Height 를 함께 내려야 한다 — 안 그러면 선택 행만 4px 어긋난다(설계 검토에서 잡은 실제 결함).
[라벨] 폭 86→92 + 줄바꿈. 실측상 가장 긴 라벨이 11자("입력 파라미터 컨트롤")라 86px 에서
"액션 대상 컨…" 처럼 잘렸다. 자르면 툴팁을 띄워야 아는 정보가 되므로 접는 쪽을 택했다.
[그 밖에] 태그·마스크 편집기를 여는 "…" 버튼은 26px 칩 옆에서 점 하나로 보여 아이콘 버튼으로.
섹션 헤더 건수를 줄 수가 아니라 **속성 수**로(LeafCount) — 쌍/세그먼트는 한 줄이지만 속성은
여럿이라 그냥 세면 "공통 3"으로 실제보다 적게 나온다.
인스펙터 패널 최소폭 220→250 — 스타일 행이 라벨 92 + 토글 4칸 128 = 220px 를 요구한다.
효과: 공통 5행→3행, 글꼴 4행→3행(속성은 4→6개로 늘었다). TextBox 기준 25행→18행.
'전체 속성 표시' 토글이 기본 창에서 스크롤 없이 보인다(종전에는 한참 아래).
검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 픽셀 차이 0. 인스펙터를 300px 호스트 창에 담아 양 테마로 렌더해 배치 확인
(dialog-shots 진단에 09/10 추가 — 속성 패널 레이아웃 회귀를 눈으로 잡는 자동 경로).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
509 lines
17 KiB
C#
509 lines
17 KiB
C#
using System.Windows.Media;
|
|
using SheetMe.Designer.ViewModels.Controls;
|
|
|
|
namespace SheetMe.Designer.ViewModels.Inspector;
|
|
|
|
/// <summary>
|
|
/// 인스펙터 행 ViewModel — 라벨 + 문자열 정규화 값. 커밋 시(값 변경 시에만) 소유자 콜백으로
|
|
/// Undo 스냅샷 → 전체 선택 대상 적용 → 시각 재해석이 수행된다.
|
|
/// </summary>
|
|
public abstract class PropertyRowViewModel : ViewModelBase
|
|
{
|
|
#region Member Fields
|
|
private string valueText = string.Empty;
|
|
private bool building;
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary>행 라벨(한글)</summary>
|
|
public string Label { get; }
|
|
|
|
/// <summary>선택 대상들의 값이 서로 다른지 — "여러 값" 표시</summary>
|
|
public bool IsMixed { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 이 행이 품고 있는 '속성' 개수 — 섹션 헤더의 건수 표시용.
|
|
/// 쌍 행·세그먼트 행은 한 줄이지만 속성은 여럿이라, 줄 수를 세면 헤더가 "공통 3"처럼
|
|
/// 실제보다 적게 나온다. 사용자가 세는 것은 줄이 아니라 속성이다.
|
|
/// </summary>
|
|
public virtual int LeafCount => 1;
|
|
|
|
/// <summary>소속 섹션 — null 이면 섹션 밖(항상 표시)</summary>
|
|
public SectionRowViewModel? Section { get; set; }
|
|
|
|
/// <summary>접힌 섹션에 속하면 숨긴다 — ItemContainerStyle 이 바인딩</summary>
|
|
public bool IsRowVisible => Section is null || Section.IsExpanded;
|
|
|
|
/// <summary>표시 여부 통지(섹션 접기/펴기)</summary>
|
|
public void NotifyVisibilityChanged() => OnPropertyChanged(nameof(IsRowVisible));
|
|
|
|
/// <summary>정규화 문자열 값 — 파생 편집기가 형 변환</summary>
|
|
public string ValueText
|
|
{
|
|
get => valueText;
|
|
set
|
|
{
|
|
if (!SetProperty(ref valueText, value) || building)
|
|
{
|
|
return;
|
|
}
|
|
IsMixed = false;
|
|
OnPropertyChanged(nameof(IsMixed));
|
|
Commit?.Invoke(value);
|
|
OnValueApplied();
|
|
}
|
|
}
|
|
|
|
/// <summary>커밋 콜백 — InspectorViewModel 이 배선(스냅샷+적용)</summary>
|
|
public Action<string>? Commit { get; set; }
|
|
#endregion
|
|
|
|
#region Constructors
|
|
protected PropertyRowViewModel(string label)
|
|
{
|
|
Label = label;
|
|
}
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>초기값 세팅(커밋 미발생)</summary>
|
|
public void Initialize(string? value, bool isMixed = false)
|
|
{
|
|
building = true;
|
|
valueText = value ?? string.Empty;
|
|
IsMixed = isMixed;
|
|
OnPropertyChanged(nameof(ValueText));
|
|
OnPropertyChanged(nameof(IsMixed));
|
|
building = false;
|
|
// 파생 표시(토글 3상태·미리보기·요약)도 초기값 기준으로 맞춘다.
|
|
// 드래그 중 RefreshBoundsRows 가 Initialize 만 호출하는 경로가 있어 여기서 하지 않으면 파생이 뒤처진다.
|
|
OnValueApplied();
|
|
}
|
|
|
|
/// <summary>값 적용 후 파생 갱신 지점</summary>
|
|
protected virtual void OnValueApplied() { }
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// 구분 헤더 행 — 클릭으로 접기/펴기.
|
|
///
|
|
/// Rows 는 평면 컬렉션을 유지하고(다중선택 재구성이 잦아 트리 재구축 비용을 피한다) 섹션이 자기
|
|
/// 소속 행을 들고 있다가 IsRowVisible 만 갱신한다. 컨테이너 Visibility 만 바뀌므로 편집 중이던
|
|
/// 값·포커스 상태가 보존된다.
|
|
/// </summary>
|
|
public sealed class SectionRowViewModel : PropertyRowViewModel
|
|
{
|
|
private bool isExpanded = true;
|
|
|
|
/// <summary>이 섹션에 속한 행들</summary>
|
|
public List<PropertyRowViewModel> Children { get; } = new();
|
|
|
|
/// <summary>펼침 상태</summary>
|
|
public bool IsExpanded
|
|
{
|
|
get => isExpanded;
|
|
set
|
|
{
|
|
if (!SetProperty(ref isExpanded, value))
|
|
{
|
|
return;
|
|
}
|
|
OnPropertyChanged(nameof(ChevronIcon));
|
|
foreach (var child in Children)
|
|
{
|
|
child.NotifyVisibilityChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>펼침 표시 아이콘</summary>
|
|
public string ChevronIcon => isExpanded ? "chevron-down" : "chevron-right";
|
|
|
|
/// <summary>접힌 상태에서 몇 개가 숨어 있는지</summary>
|
|
public string CountText
|
|
{
|
|
get
|
|
{
|
|
var total = Children.Sum(c => c.LeafCount);
|
|
return total == 0 ? string.Empty : total.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>헤더 클릭 — 접기/펴기</summary>
|
|
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; }
|
|
|
|
public SectionRowViewModel(string label) : base(label)
|
|
{
|
|
ToggleCommand = new M.Framework.WPF.Command((sender, e) => IsExpanded = !IsExpanded);
|
|
}
|
|
|
|
/// <summary>행 편입 — InspectorViewModel 이 Rows.Add 와 함께 호출</summary>
|
|
public void Adopt(PropertyRowViewModel row)
|
|
{
|
|
row.Section = this;
|
|
Children.Add(row);
|
|
OnPropertyChanged(nameof(CountText));
|
|
}
|
|
}
|
|
|
|
/// <summary>한 줄 문자열 행</summary>
|
|
public sealed class TextRowViewModel : PropertyRowViewModel
|
|
{
|
|
public TextRowViewModel(string label) : base(label) { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 두 값을 한 줄에 나란히 두는 행 — X|Y, 너비|높이.
|
|
///
|
|
/// 자식 행은 <b>평범한 행 그대로</b>다. 값·커밋·"여러 값" 판정이 전부 자식 안에서 끝나므로
|
|
/// 커밋 규약(Initialize 는 Commit 을 내지 않고, 값이 바뀔 때만 Undo 스냅샷 1회)이 그대로 유지된다.
|
|
/// 한쪽만 값이 갈리는 다중선택도 자연히 옳게 나온다.
|
|
/// 이 행 자신은 컨테이너일 뿐이라 Commit 을 갖지 않는다.
|
|
/// </summary>
|
|
public sealed class PairRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>왼쪽 칸 라벨(짧게 — X, 너비)</summary>
|
|
public string LeftLabel { get; }
|
|
|
|
/// <summary>왼쪽 칸 행</summary>
|
|
public PropertyRowViewModel Left { get; }
|
|
|
|
/// <summary>오른쪽 칸 라벨</summary>
|
|
public string RightLabel { get; }
|
|
|
|
/// <summary>오른쪽 칸 행</summary>
|
|
public PropertyRowViewModel Right { get; }
|
|
|
|
/// <summary>한 줄이지만 속성은 둘</summary>
|
|
public override int LeafCount => 2;
|
|
|
|
public PairRowViewModel(string leftLabel, PropertyRowViewModel left, string rightLabel, PropertyRowViewModel right)
|
|
: base(leftLabel + "/" + rightLabel)
|
|
{
|
|
LeftLabel = leftLabel;
|
|
Left = left;
|
|
RightLabel = rightLabel;
|
|
Right = right;
|
|
}
|
|
}
|
|
|
|
/// <summary>세그먼트 한 칸 — 아이콘 토글 하나</summary>
|
|
public sealed class SegmentItem
|
|
{
|
|
/// <summary>Lucide 아이콘명</summary>
|
|
public string Icon { get; }
|
|
|
|
/// <summary>툴팁(라벨 대용 — 아이콘만으로는 뜻이 안 서는 사용자를 위해)</summary>
|
|
public string ToolTip { get; }
|
|
|
|
/// <summary>실제 값을 들고 있는 토글 행</summary>
|
|
public ToggleRowViewModel Toggle { get; }
|
|
|
|
public SegmentItem(string icon, string toolTip, ToggleRowViewModel toggle)
|
|
{
|
|
Icon = icon;
|
|
ToolTip = toolTip;
|
|
Toggle = toggle;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 토글 묶음을 한 줄 아이콘 세그먼트로 — 굵게·기울임·밑줄.
|
|
/// 체크박스 하나에 한 줄씩 쓰면 세 줄(약 100px)을 먹는데, 서로 배타적이지 않은 같은 축의
|
|
/// 속성이라 한 줄에 모으는 편이 읽기도 쉽다. <see cref="PairRowViewModel"/> 과 같은 이유로
|
|
/// 값은 전부 자식 토글이 들고 있다.
|
|
/// </summary>
|
|
public sealed class SegmentRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>세그먼트 칸들</summary>
|
|
public List<SegmentItem> Items { get; } = new();
|
|
|
|
/// <summary>한 줄이지만 속성은 칸 수만큼</summary>
|
|
public override int LeafCount => Items.Count;
|
|
|
|
public SegmentRowViewModel(string label) : base(label) { }
|
|
}
|
|
|
|
/// <summary>여러 줄 문자열 행(Text/수식/항목 목록)</summary>
|
|
public sealed class MultilineTextRowViewModel : PropertyRowViewModel
|
|
{
|
|
public MultilineTextRowViewModel(string label) : base(label) { }
|
|
}
|
|
|
|
/// <summary>숫자 행 — 문자열 바인딩, 커밋 시 숫자 검증은 소유자에서</summary>
|
|
public sealed class NumberRowViewModel : PropertyRowViewModel
|
|
{
|
|
public NumberRowViewModel(string label) : base(label) { }
|
|
}
|
|
|
|
/// <summary>토글(참/거짓) 행</summary>
|
|
public sealed class ToggleRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>
|
|
/// 체크 상태 — "True"/"False" 문자열과 동기. 다중선택에서 값이 갈리면 null(불확정)이다.
|
|
/// false 로 내리면 "전부 꺼짐"으로 보이고, 사용자가 한 번 눌러 켰다 끄면 전부 False 가 되어
|
|
/// 조용히 값이 뭉개진다.
|
|
/// </summary>
|
|
public bool? IsOn
|
|
{
|
|
get => IsMixed ? null : ValueText == "True";
|
|
set
|
|
{
|
|
if (value is bool on)
|
|
{
|
|
ValueText = on ? "True" : "False";
|
|
}
|
|
}
|
|
}
|
|
|
|
public ToggleRowViewModel(string label) : base(label) { }
|
|
|
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(IsOn));
|
|
}
|
|
|
|
/// <summary>선택지 행</summary>
|
|
public sealed class ChoiceRowViewModel : PropertyRowViewModel
|
|
{
|
|
private readonly List<string> choices;
|
|
|
|
/// <summary>선택지 목록</summary>
|
|
public IReadOnlyList<string> Choices => choices;
|
|
|
|
public ChoiceRowViewModel(string label, string[] choices) : base(label)
|
|
{
|
|
this.choices = choices.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 값이 선택지에 없으면 목록에 편입한다.
|
|
/// ComboBox.SelectedItem 은 TwoWay 기본이라, 바인딩 값이 ItemsSource 에 없으면 null 로 코어스한 뒤
|
|
/// 그 null 을 소스에 되써서 <b>속성이 삭제된다</b>. 선택만 해도 데이터가 손상되므로 방어가 필요하다.
|
|
/// </summary>
|
|
public void EnsureChoice(string? value)
|
|
{
|
|
if (!string.IsNullOrEmpty(value) && !choices.Contains(value, StringComparer.Ordinal))
|
|
{
|
|
choices.Add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 태그 피커 행 — 값 표시 + 찾아보기 버튼(검색 대화상자).
|
|
/// 목록에 없는 사이트 커스텀 태그는 텍스트 직접 입력도 허용.
|
|
/// </summary>
|
|
public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>피커 선택지(레거시 카탈로그)</summary>
|
|
public IReadOnlyList<string> Choices { get; }
|
|
|
|
/// <summary>피커 대화상자 제목</summary>
|
|
public string PickerTitle { get; }
|
|
|
|
/// <summary>찾아보기 — 검색 대화상자 열기</summary>
|
|
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
|
|
|
|
public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList<string> choices) : base(label)
|
|
{
|
|
PickerTitle = pickerTitle;
|
|
Choices = choices;
|
|
BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse());
|
|
}
|
|
|
|
private void OnBrowse()
|
|
{
|
|
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText)
|
|
{
|
|
Owner = System.Windows.Application.Current.MainWindow,
|
|
};
|
|
if (dialog.ShowDialog() == true)
|
|
{
|
|
ValueText = dialog.SelectedTag ?? string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마스크 행 — 값과 표시 미리보기를 함께 보여주고, 찾아보기로 프리셋 편집기를 연다.
|
|
/// 레거시는 WinForms 기본 마스크 디자이너(프리셋 + 시험 입력)를 제공했다.
|
|
/// </summary>
|
|
public sealed class MaskRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>이 마스크가 화면에 어떻게 보이는지 — 인스펙터에서 바로 확인</summary>
|
|
public string Preview => ValueText.Length == 0
|
|
? "(마스크 없음)"
|
|
: SheetMe.Core.Serialization.LegacyMask.ToPromptDisplay(ValueText);
|
|
|
|
/// <summary>마스크 편집기 열기</summary>
|
|
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
|
|
|
|
public MaskRowViewModel(string label) : base(label)
|
|
{
|
|
BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse());
|
|
}
|
|
|
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview));
|
|
|
|
private void OnBrowse()
|
|
{
|
|
var dialog = new Views.MaskPickerDialogView(ValueText)
|
|
{
|
|
Owner = System.Windows.Application.Current.MainWindow,
|
|
};
|
|
if (dialog.ShowDialog() == true)
|
|
{
|
|
ValueText = dialog.Mask;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>SQL 쿼리 행 — 요약 표시 + 전용 편집기 창(치환 변수 삽입)</summary>
|
|
public sealed class QueryRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>요약 텍스트(한 줄)</summary>
|
|
public string Summary
|
|
{
|
|
get
|
|
{
|
|
var oneLine = ValueText.Replace("\r", " ").Replace("\n", " ").Trim();
|
|
return oneLine.Length == 0 ? "(쿼리 없음)" : oneLine.Length > 48 ? oneLine[..48] + "…" : oneLine;
|
|
}
|
|
}
|
|
|
|
/// <summary>전용 편집기 열기</summary>
|
|
public M.Framework.WPF.ICustomCommand? EditCommand { get; set; }
|
|
|
|
public QueryRowViewModel(string label) : base(label)
|
|
{
|
|
EditCommand = new M.Framework.WPF.Command((sender, e) => OnEdit());
|
|
}
|
|
|
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
|
|
|
private void OnEdit()
|
|
{
|
|
var dialog = new Views.QueryEditorWindow(Label, ValueText)
|
|
{
|
|
Owner = System.Windows.Application.Current.MainWindow,
|
|
};
|
|
if (dialog.ShowDialog() == true)
|
|
{
|
|
ValueText = dialog.QueryText;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>색 행 — 레거시 invariant 문자열("R, G, B"/명명색) + 미리보기 스와치</summary>
|
|
public sealed class ColorRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>미리보기 브러시</summary>
|
|
public Brush Preview
|
|
{
|
|
get
|
|
{
|
|
if (ValueText.Length == 0)
|
|
{
|
|
return Brushes.Transparent;
|
|
}
|
|
var (a, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
|
|
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
|
|
brush.Freeze();
|
|
return brush;
|
|
}
|
|
}
|
|
|
|
/// <summary>색상 피커 열기 — 확정 시 레거시 invariant 형식("R, G, B")으로 반영</summary>
|
|
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
|
|
|
|
public ColorRowViewModel(string label) : base(label)
|
|
{
|
|
PickCommand = new M.Framework.WPF.Command((sender, e) => OnPick());
|
|
}
|
|
|
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview));
|
|
|
|
private void OnPick()
|
|
{
|
|
var initialHex = (string?)null;
|
|
if (ValueText.Length > 0)
|
|
{
|
|
var (_, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
|
|
initialHex = $"#{r:X2}{g:X2}{b:X2}";
|
|
}
|
|
var picked = Views.ColorPickerWindow.Pick(System.Windows.Application.Current.MainWindow, initialHex);
|
|
if (picked is null)
|
|
{
|
|
return;
|
|
}
|
|
var color = (Color)ColorConverter.ConvertFromString(picked);
|
|
ValueText = Core.Serialization.LegacyFormat.FormatColor(255, color.R, color.G, color.B);
|
|
}
|
|
}
|
|
|
|
/// <summary>읽기 전용 행 — 중첩/바이너리/참조 등 raw 편집 불가 값 표시</summary>
|
|
public sealed class ReadOnlyRowViewModel : PropertyRowViewModel
|
|
{
|
|
public ReadOnlyRowViewModel(string label, string display) : base(label)
|
|
{
|
|
Initialize(display);
|
|
}
|
|
}
|
|
|
|
/// <summary>전체 속성(고급) 섹션 토글 행 — 표시/숨김 버튼</summary>
|
|
public sealed class ToggleAdvancedRowViewModel : PropertyRowViewModel
|
|
{
|
|
/// <summary>버튼 표시 텍스트</summary>
|
|
public string ButtonText { get; }
|
|
|
|
/// <summary>토글 실행</summary>
|
|
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; set; }
|
|
|
|
public ToggleAdvancedRowViewModel(string buttonText, Action toggle) : base(string.Empty)
|
|
{
|
|
ButtonText = buttonText;
|
|
ToggleCommand = new M.Framework.WPF.Command((sender, e) => toggle());
|
|
}
|
|
}
|
|
|
|
/// <summary>속성 추가 행 — 키 입력 후 빈 속성 생성(고급)</summary>
|
|
public sealed class AddPropertyRowViewModel : PropertyRowViewModel
|
|
{
|
|
private string keyText = string.Empty;
|
|
|
|
/// <summary>추가할 속성 키(레거시 Property 이름)</summary>
|
|
public string KeyText
|
|
{
|
|
get => keyText;
|
|
set => SetProperty(ref keyText, value);
|
|
}
|
|
|
|
/// <summary>추가 실행</summary>
|
|
public M.Framework.WPF.ICustomCommand? AddCommand { get; set; }
|
|
|
|
public AddPropertyRowViewModel(Action<string> add) : base(string.Empty)
|
|
{
|
|
AddCommand = new M.Framework.WPF.Command((sender, e) =>
|
|
{
|
|
var key = KeyText.Trim();
|
|
if (key.Length > 0)
|
|
{
|
|
add(key);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>인스펙터 행 컨텍스트 — 대상 컨트롤 집합과 접근자</summary>
|
|
public sealed class RowBinding
|
|
{
|
|
/// <summary>값 읽기</summary>
|
|
public required Func<ControlViewModel, string?> Get { get; init; }
|
|
|
|
/// <summary>값 쓰기</summary>
|
|
public required Action<ControlViewModel, string> Set { get; init; }
|
|
|
|
/// <summary>커밋 후 시각 재해석 필요 여부</summary>
|
|
public bool AffectsVisual { get; init; } = true;
|
|
}
|