Делегаты как свойства: плохая идея?

Рассмотрим следующий элемент управления (сокращенный для краткости):

public partial class ConfigurationManagerControl : UserControl
{

    public Func<string, bool> CanEdit { get; set;}
    public Func<string, bool> CanDelete { get; set; }

    public Dictionary<string, string> Settings
    {
        get { return InnerSettings; }
        set
        {
            InnerSettings = value;
            BindData();
        }
    }
    private Dictionary<string, string> InnerSettings;

    private void OnListIndexChanged(object sender, EventArgs e)
    {
        this.EditButton.Enabled = false;
        this.DeleteButton.Enabled = false;

        var indices = this.List.SelectedIndices;
        if (indices.Count != 1)
        {
            return;
        }

        var index = indices[0];
        var item = this.List.Items[index];

        if (this.CanEdit != null)
        {
            this.EditButton.Enabled = this.CanEdit(item.Text);
        }

        if (this.CanDelete != null)
        {
            this.DeleteButton.Enabled = this.CanDelete(item.Text);
        }

    }
}

Там больше для этого элемента управления, но достаточно сказать, что он позволяет пользователю добавлять, редактировать и удалять записи в словаре < string, string > . Чтобы определить, разрешить ли пользователю редактировать или удалять записи, он использует свойства метода делегата CanDelete и CanEdit, которые предоставляются формой или элементом управления, который его размещает:

public class SetupWizard : Form
{
    public SetupWizard()
    {
        InitializeComponent();

        this.SettingManager.CanEdit = CanEditSetting;
        this.SettingManager.CanDelete = CanDeleteSetting;
    }

    private static bool CanEditSetting(string item)
    {
        var lockedSettings = new[] { "LicenseHash", "ProductHash" };
        return !lockedSettings.Contains(item.ToLower());
    }

    private static bool CanDeleteSetting(string item)
    {
        var lockedSettings = new[] {
                                        "LicenseHash",
                                        "ProductHash", 
                                        "UserName", 
                                        "CompanyName"
                                    };
        return !lockedSettings.Contains(item.ToLower());
    }
}

Я считаю, что этот дизайн одновременно является удовлетворительным и тревожным. С одной стороны, это, по-видимому, решает проблему, используя простейшее решение, которое работает (это, безусловно, отлично разделяет проблемы). С другой стороны, у меня есть это подозрительное беспокойство, что я неправильно использую делегатов и вместо этого должен использовать событие (даже если мне не нужны несколько слушателей, и мне нужен только вызывающий, чтобы сообщить мне, доступен ли элемент для редактирования).

И затем, с другой стороны, есть вероятность, что есть совершенно другой дизайн, который я даже не рассматривал, который мог бы решить проблему значительно превосходящим образом.

Итак. Является ли эта конструкция технически правильной, удобной и гибкой? Или я должен делать что-то лучше?

Ответы

Ответ 1

Я предлагаю использовать интерфейс с этими двумя методами. Это намного чище:

interface ICantThinkOfAGoodName
{
    bool CanEdit(string item);
    bool CanDelete(string item);
}

Вы можете создать нечто похожее на RelayCommand, используемое во многих фреймворках MVVM:

public class RelayObject : ICantThinkOfAGoodName
{
    public RelayObject() : this(null, null) {}
    public RelayObject(Func<string, bool> canEdit, Func<string, bool> canDelete)
    {
        if(canEdit == null) canEdit = s => true;
        if(canDelete == null) canDelete = s => true;

        _canEdit = canEdit;
        _canDelete = canDelete;
    }

    public bool CanEdit(string item)
    {
        return _canEdit(item);
    }
    public bool CanDelete(string item)
    {
        return _canDelete(item);
    }
}

Используйте его следующим образом:

public SetupWizard()
{
    InitializeComponent();

    this.SettingManager.PropertyName = new RelayObject(CanEditSetting, 
                                                       CanDeleteSetting);
    // or (all can be deleted)
    this.SettingManager.PropertyName = new RelayObject(CanEditSetting, null);
    // or (all can be edited)
    this.SettingManager.PropertyName = new RelayObject(null, CanDeleteSetting);
    // or (all can be edited and deleted)
    this.SettingManager.PropertyName = new RelayObject();

}

BTW: здесь я использую Property injection, потому что это элемент управления. Обычно я передавал зависимость ICantThinkOfAGoodName в конструкторе ConfigurationManagerControl.

Ответ 2

Возможно, это то, о чем говорит @Daniel Hilgarth, когда говорит "использовать интерфейс" (n.b. - его ответ теперь отражает более общий/гибкий подход к реализации интерфейса). Вместо того, чтобы напрямую назначать делегатам ваш метод, почему бы не дать элементу управления свойство, например DataState или тому, что вы хотите назвать, используя интерфейс, который инкапсулирует необходимую вам информацию, и оставьте его владельцу, чтобы решить, как для реализации этого.

interface IDataState
{
    bool CanEdit(string item);
    bool CanDelete(string item);
}

public partial class ConfigurationManagerControl : UserControl
{
    public IDataState DataState {get;set;}
    // your code checks DataState.CanEdit & DataState.CanDelete
}

public class SetupWizard : Form, IDataState
{
    public SetupWizard()
    {
        InitializeComponent();
        SettingManager.DataState =this;
    }
    public bool CanEdit(string item)
    { 
        ... implement directly or return from your private function
    }
    public bool CanDelete(string item)
    { 

    }
}

Но это дает вам гибкость в реализации этого интерфейса любым способом, который вы выбираете, с другим объектом и т.д., и это упрощает также просто передачу самого владельца (реализация интерфейса).