ui 바인딩 로직 수정

This commit is contained in:
NTG 2025-08-25 05:12:05 +09:00
parent acb4a8a170
commit 3e93ad0b39
11 changed files with 303 additions and 126 deletions

View File

@ -53,10 +53,8 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_viewImage: {fileID: 5168542205125039251} _viewImage: {fileID: 5168542205125039251}
_nameLabel: {fileID: 3228882035781370616} _nameLocalizeStringEvent: {fileID: 8756294767538431005}
_labelLocalizer: {fileID: 8756294767538431005} _descriptionLocalizeStringEvent: {fileID: 7808277355010082239}
_descriptionLabel: {fileID: 4113829672213848922}
_descriptionLocalizer: {fileID: 7808277355010082239}
_cookWarePanel: {fileID: 4302691437602401827} _cookWarePanel: {fileID: 4302691437602401827}
_cookwareImage: {fileID: 3915032697630876799} _cookwareImage: {fileID: 3915032697630876799}
_tasteHashTagPanel: {fileID: 5646014643746221419} _tasteHashTagPanel: {fileID: 5646014643746221419}
@ -9938,17 +9936,6 @@ RectTransform:
m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}
m_PrefabInstance: {fileID: 7850329751525360396} m_PrefabInstance: {fileID: 7850329751525360396}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
--- !u!114 &4113829672213848922 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6189840460486090838, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}
m_PrefabInstance: {fileID: 7850329751525360396}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &7808277355010082239 stripped --- !u!114 &7808277355010082239 stripped
MonoBehaviour: MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}
@ -11320,17 +11307,6 @@ RectTransform:
m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}
m_PrefabInstance: {fileID: 8730773236163191470} m_PrefabInstance: {fileID: 8730773236163191470}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
--- !u!114 &3228882035781370616 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 6189840460486090838, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}
m_PrefabInstance: {fileID: 8730773236163191470}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &8756294767538431005 stripped --- !u!114 &8756294767538431005 stripped
MonoBehaviour: MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3}

View File

@ -1,17 +1,60 @@
using System;
using System.Collections.Generic;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Components; using UnityEngine.Localization.Components;
using UnityEngine.UI; using UnityEngine.UI;
namespace DDD namespace DDD
{ {
public sealed class ItemDetailSnapshot
{
public Sprite BackgroundSprite { get; }
public bool ShowTastePanel { get; }
public bool ShowCookwarePanel { get; }
public Sprite CookwareSprite { get; }
public LocalizedString Name { get; }
public LocalizedString Description { get; }
public IReadOnlyList<TasteData> Tastes { get; }
public Material TasteMaterial { get; }
public ItemDetailSnapshot(
Sprite backgroundSprite,
bool showTastePanel,
bool showCookwarePanel,
Sprite cookwareSprite,
LocalizedString name,
LocalizedString description,
IReadOnlyList<TasteData> tastes,
Material tasteMaterial)
{
BackgroundSprite = backgroundSprite;
ShowTastePanel = showTastePanel;
ShowCookwarePanel = showCookwarePanel;
CookwareSprite = cookwareSprite;
Name = name;
Description = description;
Tastes = tastes;
TasteMaterial = tasteMaterial;
}
public static readonly ItemDetailSnapshot Empty = new(
backgroundSprite: null,
showTastePanel: false,
showCookwarePanel: false,
cookwareSprite: null,
name: null,
description: null,
tastes: System.Array.Empty<TasteData>(),
tasteMaterial: null);
}
public class ItemDetailView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<ItemSlotSelectedEvent> public class ItemDetailView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<ItemSlotSelectedEvent>
{ {
[SerializeField] private Image _viewImage; [SerializeField] private Image _viewImage;
[SerializeField] private TextMeshProUGUI _nameLabel; [SerializeField] private LocalizeStringEvent _nameLocalizeStringEvent;
[SerializeField] private LocalizeStringEvent _labelLocalizer; [SerializeField] private LocalizeStringEvent _descriptionLocalizeStringEvent;
[SerializeField] private TextMeshProUGUI _descriptionLabel;
[SerializeField] private LocalizeStringEvent _descriptionLocalizer;
[SerializeField] private Transform _cookWarePanel; [SerializeField] private Transform _cookWarePanel;
[SerializeField] private Image _cookwareImage; [SerializeField] private Image _cookwareImage;
[SerializeField] private Transform _tasteHashTagPanel; [SerializeField] private Transform _tasteHashTagPanel;
@ -21,51 +64,53 @@ public class ItemDetailView : MonoBehaviour, IUiView<RestaurantManagementViewMod
private RestaurantManagementViewModel _viewModel; private RestaurantManagementViewModel _viewModel;
private void OnDestroy()
{
EventBus.Unregister<ItemSlotSelectedEvent>(this);
}
public void Initialize(RestaurantManagementViewModel viewModel) public void Initialize(RestaurantManagementViewModel viewModel)
{ {
_viewModel = viewModel; _viewModel = viewModel;
_nameLabel.text = string.Empty;
_descriptionLabel.text = string.Empty;
_cookwareImage.sprite = null; _cookwareImage.sprite = null;
EventBus.Register<ItemSlotSelectedEvent>(this);
}
public void SetupBindings(BindingContext bindingContext)
{
BindingHelper.BindImage<RestaurantManagementViewModel>(bindingContext, _viewImage, viewModel => viewModel.ItemDetail.BackgroundSprite);
BindingHelper.BindImage<RestaurantManagementViewModel>(bindingContext, _cookwareImage, viewModel => viewModel.ItemDetail.CookwareSprite);
BindingHelper.BindActive<RestaurantManagementViewModel>(bindingContext, _tasteHashTagPanel.gameObject, viewModel => viewModel.ItemDetail.ShowTastePanel);
BindingHelper.BindActive<RestaurantManagementViewModel>(bindingContext, _cookWarePanel.gameObject, viewModel => viewModel.ItemDetail.ShowCookwarePanel);
BindingHelper.BindLocalizedStringEvent<RestaurantManagementViewModel>(bindingContext, _nameLocalizeStringEvent, viewModel => viewModel.ItemDetail.Name);
BindingHelper.BindLocalizedStringEvent<RestaurantManagementViewModel>(bindingContext, _descriptionLocalizeStringEvent, viewModel => viewModel.ItemDetail.Description);
} }
public void OnOpenedEvents() public void OnOpenedEvents()
{ {
UpdateView(); UpdateView();
_viewModel.OnCategoryChanged += UpdateCategory; _viewModel.OnCategoryChanged += HandleCategoryChanged;
EventBus.Register<ItemSlotSelectedEvent>(this);
} }
public void OnClosedEvents() public void OnClosedEvents()
{ {
if (_viewModel) if (_viewModel)
{ {
_viewModel.OnCategoryChanged -= UpdateCategory; _viewModel.OnCategoryChanged -= HandleCategoryChanged;
} }
EventBus.Unregister<ItemSlotSelectedEvent>(this);
} }
public void UpdateView() public void UpdateView()
{ {
UpdateCategory(_viewModel.CurrentCategory);
if (_viewModel.SelectedItem == null) if (_viewModel.SelectedItem == null)
{ {
_labelLocalizer.StringReference = null;
_descriptionLocalizer.StringReference = null;
_cookwareImage.sprite = null;
ClearHashTags(); ClearHashTags();
return; return;
} }
_labelLocalizer.StringReference = _viewModel.GetItemName();
_descriptionLocalizer.StringReference = _viewModel.GetItemDescription();
_cookwareImage.sprite = _viewModel.GetCookwareSprite();
UpdateTasteHashTags(); UpdateTasteHashTags();
} }
@ -101,15 +146,8 @@ private void UpdateTasteHashTags()
LayoutRebuilder.ForceRebuildLayoutImmediate(_tasteHashTagContent2); LayoutRebuilder.ForceRebuildLayoutImmediate(_tasteHashTagContent2);
} }
public void UpdateCategory(InventoryCategoryType category) public void HandleCategoryChanged(InventoryCategoryType category)
{ {
_viewImage.sprite = _viewModel.GetDetailBackground(category);
bool showTaste = _viewModel.ShouldShowTaste(category);
bool showCookware = _viewModel.ShouldShowCookware(category);
_tasteHashTagPanel.gameObject.SetActive(showTaste);
_cookWarePanel.gameObject.SetActive(showCookware);
Canvas.ForceUpdateCanvases(); Canvas.ForceUpdateCanvases();
} }

View File

@ -123,6 +123,8 @@ protected override GameObject GetInitialSelected()
protected override void SetupBindings() protected override void SetupBindings()
{ {
BindingHelper.BindImageFilled(_bindingContext, _completeBatchFilledImage, nameof(RestaurantManagementViewModel.NormalizedHoldProgress)); BindingHelper.BindImageFilled(_bindingContext, _completeBatchFilledImage, nameof(RestaurantManagementViewModel.NormalizedHoldProgress));
_itemDetailView.SetupBindings(_bindingContext);
} }
private void InitializeViews() private void InitializeViews()

View File

@ -173,7 +173,10 @@ public void SetCategory(InventoryCategoryType category)
{ {
if (CurrentCategory == category) return; if (CurrentCategory == category) return;
BeginUpdate();
CurrentCategory = category; CurrentCategory = category;
RecomputeItemDetail();
EndUpdate();
OnCategoryChanged?.Invoke(category); OnCategoryChanged?.Invoke(category);
} }
@ -365,56 +368,68 @@ public void OnInventoryChanged()
#endregion #endregion
#region ItemDetailView #region ItemDetailView
private ItemDetailSnapshot _itemDetail = ItemDetailSnapshot.Empty;
public ItemDetailSnapshot ItemDetail
{
get => _itemDetail;
private set => SetField(ref _itemDetail, value);
}
public ItemViewModel SelectedItem { get; private set; }
private const string CookwareDetailPanel = "CookwareDetailPanel"; private const string CookwareDetailPanel = "CookwareDetailPanel";
private const string IngredientDetailPanel = "IngredientDetailPanel"; private const string IngredientDetailPanel = "IngredientDetailPanel";
private const string RecipeDetailPanel = "RecipeDetailPanel"; private const string RecipeDetailPanel = "RecipeDetailPanel";
public ItemViewModel SelectedItem { get; private set; }
public void SetSelectedItem(ItemViewModel item) public void SetSelectedItem(ItemViewModel item)
{ {
if (SelectedItem == item) return; if (SelectedItem == item) return;
BeginUpdate();
SelectedItem = item; SelectedItem = item;
RecomputeItemDetail();
EndUpdate();
} }
public TasteHashTagSlotUi CreateHashTag(RectTransform parent) public TasteHashTagSlotUi CreateHashTag(RectTransform parent)
{ {
return Instantiate(GetRestaurantManagementData().TasteHashTagSlotUiPrefab, parent, false); return Instantiate(GetRestaurantManagementData().TasteHashTagSlotUiPrefab, parent, false);
} }
private string GetViewItemKey() private void RecomputeItemDetail()
{ {
return SelectedItem.ItemType == ItemType.Recipe ? SelectedItem.GetRecipeResultKey : SelectedItem.Id; var background = CurrentCategory switch
}
public LocalizedString GetItemName()
{
var key = GetViewItemKey();
return LocalizationManager.Instance.GetLocalizedName(key);
}
public LocalizedString GetItemDescription()
{
var key = GetViewItemKey();
return LocalizationManager.Instance.GetLocalizedDescription(key);
}
public Sprite GetDetailBackground(InventoryCategoryType category)
{
return category switch
{ {
InventoryCategoryType.Food or InventoryCategoryType.Drink InventoryCategoryType.Food or InventoryCategoryType.Drink => DataManager.Instance.GetSprite(RecipeDetailPanel),
=> DataManager.Instance.GetSprite(RecipeDetailPanel), InventoryCategoryType.Ingredient => DataManager.Instance.GetSprite(IngredientDetailPanel),
InventoryCategoryType.Ingredient InventoryCategoryType.Cookware or InventoryCategoryType.Special => DataManager.Instance.GetSprite(CookwareDetailPanel),
=> DataManager.Instance.GetSprite(IngredientDetailPanel), _ => null
InventoryCategoryType.Cookware or InventoryCategoryType.Special };
=> DataManager.Instance.GetSprite(CookwareDetailPanel), bool showTaste = CurrentCategory is InventoryCategoryType.Food or InventoryCategoryType.Drink or InventoryCategoryType.Ingredient;
bool showCookware = CurrentCategory is InventoryCategoryType.Food or InventoryCategoryType.Drink;
var cookwareSprite = SelectedItem.GetCookwareIcon;
string key = SelectedItem.ItemType == ItemType.Recipe ? SelectedItem.GetRecipeResultKey : SelectedItem.Id;
var nameLocalizedString = LocalizationManager.Instance.GetLocalizedName(key);
var descriptionLocalizedString = LocalizationManager.Instance.GetLocalizedDescription(key);
var tastes = SelectedItem.GetTasteDatas;
var tasteMat = SelectedItem.RecipeType switch
{
RecipeType.FoodRecipe => RestaurantData.Instance.ManagementData.FoodTasteMaterial,
RecipeType.DrinkRecipe => RestaurantData.Instance.ManagementData.DrinkTasteMaterial,
_ => null _ => null
}; };
}
public Sprite GetCookwareSprite() => SelectedItem.GetCookwareIcon; ItemDetail = new ItemDetailSnapshot(
backgroundSprite: background,
showTastePanel: showTaste,
showCookwarePanel: showCookware,
cookwareSprite: cookwareSprite,
name: nameLocalizedString,
description: descriptionLocalizedString,
tastes: tastes,
tasteMaterial: tasteMat);
}
public IReadOnlyList<TasteData> GetTastes() => SelectedItem?.GetTasteDatas; public IReadOnlyList<TasteData> GetTastes() => SelectedItem?.GetTasteDatas;
@ -430,12 +445,6 @@ public Material GetTasteMaterial()
}; };
} }
public bool ShouldShowTaste(InventoryCategoryType category)
=> category is InventoryCategoryType.Food or InventoryCategoryType.Drink or InventoryCategoryType.Ingredient;
public bool ShouldShowCookware(InventoryCategoryType category)
=> category is InventoryCategoryType.Food or InventoryCategoryType.Drink;
#endregion #endregion
#region TodayMenuView #region TodayMenuView

View File

@ -8,9 +8,6 @@ public class TodayRestaurantStateView : MonoBehaviour, IUiView<RestaurantManagem
[SerializeField] private Transform _todayWorkerContent; [SerializeField] private Transform _todayWorkerContent;
[SerializeField] private Transform _todayCookwareContent; [SerializeField] private Transform _todayCookwareContent;
private List<ItemSlotUi> _workerSlots;
private List<ItemSlotUi> _cookwareSlots;
private RestaurantManagementViewModel _viewModel; private RestaurantManagementViewModel _viewModel;
public void Initialize(RestaurantManagementViewModel viewModel) public void Initialize(RestaurantManagementViewModel viewModel)

View File

@ -9,12 +9,6 @@ public abstract class SimpleViewModel : MonoBehaviour, INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
protected virtual void Awake() { }
protected virtual void OnEnable() { }
protected virtual void Start() { }
protected virtual void OnDisable() { }
protected virtual void OnDestroy() { }
public virtual void Initialize() { } public virtual void Initialize() { }
public virtual void Cleanup() { } public virtual void Cleanup() { }
@ -24,6 +18,15 @@ public virtual void Cleanup() { }
/// <param name="propertyName">변경된 속성 이름 (자동으로 설정됨)</param> /// <param name="propertyName">변경된 속성 이름 (자동으로 설정됨)</param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{ {
if (string.IsNullOrEmpty(propertyName)) return;
if (_updateDepth > 0)
{
// 배치 업데이트 중: 나중에 일괄 발행
_pendingNotifications.Add(propertyName);
return;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
@ -43,35 +46,38 @@ protected bool SetField<T>(ref T field, T value, [CallerMemberName] string prope
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);
return true; return true;
} }
/// <summary>
/// 배치 업데이트를 위한 플래그
/// </summary>
private bool _isUpdating;
/// <summary> private int _updateDepth;
/// 배치 업데이트 중 보류된 알림들
/// </summary>
private readonly HashSet<string> _pendingNotifications = new(); private readonly HashSet<string> _pendingNotifications = new();
protected void BeginUpdate()
{
_updateDepth++;
}
/// <summary>
/// 배치 업데이트 시작 - 여러 속성 변경을 한 번에 처리
/// </summary>
protected void BeginUpdate() => _isUpdating = true;
/// <summary>
/// 배치 업데이트 종료 - 보류된 모든 알림을 처리
/// </summary>
protected void EndUpdate() protected void EndUpdate()
{ {
_isUpdating = false; if (_updateDepth == 0)
{
Debug.LogWarning("EndUpdate called without matching BeginUpdate.");
return;
}
_updateDepth--;
// 아직 상위 배치가 진행 중이면 플러시하지 않음
if (_updateDepth > 0) return;
if (_pendingNotifications.Count > 0) if (_pendingNotifications.Count > 0)
{ {
foreach (var prop in _pendingNotifications) // 복사 후 클리어(핸들러 내부에서 BeginUpdate가 호출되어도 안전)
{ var toNotify = new List<string>(_pendingNotifications);
OnPropertyChanged(prop);
}
_pendingNotifications.Clear(); _pendingNotifications.Clear();
foreach (var prop in toNotify)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
} }
} }
} }

View File

@ -1,10 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace DDD namespace DDD
{ {
@ -16,6 +14,12 @@ public class BindingContext
private readonly Dictionary<string, List<IBindingTarget>> _bindings = new(); private readonly Dictionary<string, List<IBindingTarget>> _bindings = new();
private readonly Dictionary<string, IValueConverter> _converters = new(); private readonly Dictionary<string, IValueConverter> _converters = new();
private INotifyPropertyChanged _dataContext; private INotifyPropertyChanged _dataContext;
public void Bind<TVm, TProp>(Expression<Func<TVm, TProp>> property, IBindingTarget target, IValueConverter converter = null)
{
var path = BindingPath.For(property);
Bind(path, target, converter);
}
/// <summary> /// <summary>
/// 데이터 컨텍스트 (ViewModel) 설정 /// 데이터 컨텍스트 (ViewModel) 설정
@ -61,6 +65,14 @@ public void Bind(string propertyPath, IBindingTarget target, IValueConverter con
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
UpdateBinding(e.PropertyName); UpdateBinding(e.PropertyName);
var prefix = e.PropertyName + ".";
foreach (var key in _bindings.Keys)
{
if (key.StartsWith(prefix, StringComparison.Ordinal))
{
UpdateBinding(key);
}
}
} }
/// <summary> /// <summary>

View File

@ -1,5 +1,8 @@
using System;
using System.Linq.Expressions;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.UI; using UnityEngine.UI;
namespace DDD namespace DDD
@ -11,6 +14,12 @@ public static void BindText(BindingContext context, TextMeshProUGUI text, string
var target = new TextBindingTarget(text, propertyPath); var target = new TextBindingTarget(text, propertyPath);
context?.Bind(propertyPath, target, converter); context?.Bind(propertyPath, target, converter);
} }
public static void BindLocalizedStringEvent(BindingContext context, LocalizeStringEvent localizeEvent, string propertyPath, IValueConverter converter = null)
{
var target = new LocalizeStringEventBindingTarget(localizeEvent, propertyPath);
context?.Bind(propertyPath, target, converter);
}
public static void BindImage(BindingContext context, Image image, string propertyPath, IValueConverter converter = null) public static void BindImage(BindingContext context, Image image, string propertyPath, IValueConverter converter = null)
{ {
@ -35,5 +44,41 @@ public static void BindSlider(BindingContext context, Slider slider, string prop
var target = new SliderBindingTarget(slider, propertyPath); var target = new SliderBindingTarget(slider, propertyPath);
context?.Bind(propertyPath, target, converter); context?.Bind(propertyPath, target, converter);
} }
public static void BindText<TVm>(BindingContext ctx, TextMeshProUGUI text,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindText(ctx, text, BindingPath.For(prop), conv);
public static void BindLocalizedStringEvent<TVm>(BindingContext ctx, LocalizeStringEvent loc,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindLocalizedStringEvent(ctx, loc, BindingPath.For(prop), conv);
public static void BindImage<TVm>(BindingContext ctx, Image img,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindImage(ctx, img, BindingPath.For(prop), conv);
public static void BindImageFilled<TVm>(BindingContext ctx, Image img,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindImageFilled(ctx, img, BindingPath.For(prop), conv);
public static void BindActive<TVm>(BindingContext ctx, GameObject go,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindActive(ctx, go, BindingPath.For(prop), conv);
public static void BindSlider<TVm>(BindingContext ctx, Slider slider,
Expression<Func<TVm, object>> prop, IValueConverter conv = null)
=> BindSlider(ctx, slider, BindingPath.For(prop), conv);
public static void BindImage<TVm, TProp>(BindingContext ctx, Image img,
Expression<Func<TVm, TProp>> prop, IValueConverter conv = null)
=> BindImage(ctx, img, BindingPath.For(prop), conv);
public static void BindActive<TVm, TProp>(BindingContext ctx, GameObject go,
Expression<Func<TVm, TProp>> prop, IValueConverter conv = null)
=> BindActive(ctx, go, BindingPath.For(prop), conv);
public static void BindLocalizedStringEvent<TVm, TProp>(BindingContext ctx, LocalizeStringEvent loc,
Expression<Func<TVm, TProp>> prop, IValueConverter conv = null)
=> BindLocalizedStringEvent(ctx, loc, BindingPath.For(prop), conv);
} }
} }

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace DDD
{
public static class BindingPath
{
public static string For<TViewModel, TProp>(Expression<Func<TViewModel, TProp>> expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
Expression expr = expression.Body;
// 값형식 → object 캐스팅 제거
if (expr is UnaryExpression unary && unary.NodeType == ExpressionType.Convert)
{
expr = unary.Operand;
}
var members = new Stack<string>();
while (expr is MemberExpression me)
{
members.Push(me.Member.Name);
expr = me.Expression;
if (expr is ParameterExpression) break; // 루트 도달
if (expr is UnaryExpression innerUnary && innerUnary.NodeType == ExpressionType.Convert)
expr = innerUnary.Operand;
}
return string.Join(".", members);
}
// 편의용: object 리턴 시그니처도 지원
public static string For<TViewModel>(Expression<Func<TViewModel, object>> expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
Expression expr = expression.Body;
if (expr is UnaryExpression unary && unary.NodeType == ExpressionType.Convert)
expr = unary.Operand;
var members = new Stack<string>();
while (expr is MemberExpression me)
{
members.Push(me.Member.Name);
expr = me.Expression;
if (expr is ParameterExpression) break;
if (expr is UnaryExpression innerUnary && innerUnary.NodeType == ExpressionType.Convert)
expr = innerUnary.Operand;
}
return string.Join(".", members);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 35fee64a12cf410e8758bef3294aa1ee
timeCreated: 1756065063

View File

@ -1,5 +1,7 @@
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Components;
using UnityEngine.UI; using UnityEngine.UI;
namespace DDD namespace DDD
@ -38,6 +40,33 @@ public void UpdateValue(object value)
} }
} }
public class LocalizeStringEventBindingTarget : IBindingTarget
{
private readonly LocalizeStringEvent _localizeEvent;
public string PropertyPath { get; }
public LocalizeStringEventBindingTarget(LocalizeStringEvent localizeEvent, string propertyPath)
{
_localizeEvent = localizeEvent;
PropertyPath = propertyPath;
}
public void UpdateValue(object value)
{
if (_localizeEvent == null) return;
if (value is LocalizedString localized)
{
_localizeEvent.StringReference = localized;
}
else
{
// null 또는 다른 타입인 경우 초기화
_localizeEvent.StringReference = null;
}
}
}
public class ImageBindingTarget : IBindingTarget public class ImageBindingTarget : IBindingTarget
{ {
private readonly Image _image; private readonly Image _image;
@ -51,10 +80,14 @@ public ImageBindingTarget(Image image, string propertyPath)
public void UpdateValue(object value) public void UpdateValue(object value)
{ {
if (_image != null && value is Sprite sprite) if (value is Sprite sprite)
{ {
_image.sprite = sprite; _image.sprite = sprite;
} }
else
{
_image.sprite = null;
}
} }
} }