Атрибут с действием/условием

Можно ли указать следующее:

[SomeAttribute(condition1)]
public SomeType SomeSetting1 {get; set;}

[SomeAttribute(condition2)]
public SomeType SomeSetting2 {get; set;}

где условие является чем-то сложным? К примеру,

[SomeAttribute(SomeSetting3 == 4 && SomeSetting4 < 100)]

Я использую PropertyGrid для отображения/редактирования конфигурации как свойств некоторого сериализуемого класса. И мне нужно иметь что-то вроде каскада: когда один параметр установлен, некоторые другие могут быть скрыты, в зависимости от значения.

В настоящее время я могу скрыть некоторые настройки следующим образом:

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

    public interface IHide
    {
        bool Hide { get; }
    }
    
    public class AdminAttribute : Attribute, Common.IHide
    {
        public bool Hide
        {
            get { return !MySettings.Admin; }
        }
    
        public override object TypeId { get { return "AdminAttributeId"; } }
    }
    
    // admin only setting
    [Admin]
    public SomeType SomeSetting {get; set;}
    

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

public class ElementAttribute : Attribute, Common.IHide
{
    private string _element;
    public bool Hide
    {
        get { return !Something.Instance.IsElement(_element); }
    }

    public ElementAttribute(string element)
    {
        _element = element;
    }

    public override object TypeId { get { return "ElementAttributeId"; } }
}

Используя этот атрибут, я могу указать символ элемента:

 // setting will be shown if element a present
 [Element('a')]
 public SomeType SomeSetting {get; set;}

После создания нескольких таких я пришел к мысли, что, возможно, можно как-то закодировать это условие метода Hide() в самом параметре атрибута??? Или, возможно, как-то указать поведение (действие)?

Я могу довольно легко сделать это, используя CodeDom я думаю, но это было бы очень медленно. Можно перечислить все атрибуты и условия кэширования. Но, может быть, есть более простой/альтернативный способ? Любые другие идеи?

Начальная награда

Я ищу идеи для объединения нескольких атрибутов IHide (AdminAttribute — показывать настройки, когда пользователь является администратором, ElementAttribute — показывать настройки, когда присутствует указанный элемент и т. д.) в один супер-атрибут. Я хочу иметь возможность каким-то образом указывать условия без необходимости создавать новый атрибут на основе IHide для каждого случая.

Каким было бы ваше решение, если вам нужно обрабатывать сотни настроек (которые существуют одновременно), но со связями между собой и дополнительно связанными на каких-то других условиях? Как создать поведение атрибутов Admin и Element, не создавая AdminAttribute и ElementAttribute?

Дело в том, что есть несколько разных конфигураций (унаследованных от базовой конфигурации) и я хочу иметь возможность в некоторых из них свободно задавать условия видимости частью кода, которая при оценке в false будет скрывать настройку, не создавая десятки атрибутов на основе IHide. Что-то вроде декларативного программирования при определении самой настройки!


person Sinatr    schedule 20.02.2014    source источник
comment
Я думаю, что этот вопрос SO может помочь вам в правильном направлении.   -  person Junaith    schedule 20.02.2014
comment
@Junaith, спасибо, но это больше похоже на видимость условных свойств во время выполнения, которую я уже реализовал с использованием оболочки (оболочка — это ICustomTypeDescriptor, которая создает копию только необходимых свойств для PropertyGrid). Теперь мне нужен условный атрибут для управления видимостью свойств. Что-то, что можно легко использовать в качестве декларативного атрибута для сотен свойств.   -  person Sinatr    schedule 20.02.2014
comment
Насколько я знаю, вы не можете сделать это во время компиляции. Вы не можете условно применять атрибуты на основе значений времени выполнения.   -  person Junaith    schedule 20.02.2014


Ответы (3)


Это не очень хорошая идея

Будет неоправданно сложно сделать что-то вроде того, что вы описываете, и результат не будет поддерживаться, даже если вам удастся это сделать.

Сложность связана с ограничениями на параметры атрибутов:

Параметры атрибутов ограничены постоянными значениями следующих типов:

  • Скалярные типы (bool, byte, char, short, int, long, float и double)
  • string
  • System.Type
  • перечисления
  • object (должно быть постоянным значением одного из указанных выше типов)
  • Одномерные массивы любого из вышеперечисленных типов

Очевидно, что единственный способ втиснуть предикат в любой из вышеперечисленных типов — это написать строку в стиле SQL, например

[Hide("foo = \"42\" && !bar")]
public object MyProperty { get; set; }

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

Но есть альтернативы

Ваше предпринятое решение действительно пытается плыть против текущего - атрибуты не предназначены для инкапсуляции поведения во время выполнения. Вместо этого, почему бы просто не реализовать в вашем сериализуемом классе подходящий интерфейс? Например, вы можете начать с болотного стандарта

public interface IConditionalPropertySource
{
    bool IsPropertyApplicable(string propertyName);
}

class Test : IConditionalPropertySource
{
    public string SomeSetting { get; set; }

    public bool IsPropertyApplicable(string propertyName)
    {
        switch (propertyName)
        {
            case "SomeSetting":return DateTime.Now.DayOfWeek == DayOfWeek.Friday;
            default: return false;
        }
    }
}

Это сделает работу, но у нее есть некоторые недостатки:

  1. Имена свойств не проверяются компилятором; как вызывающая сторона, так и реализация IsPropertyApplicable могут совершать ошибки (например, простые орфографические ошибки), которые не будут помечены.
  2. Не сразу понятно, какие свойства являются условными, а какие нет, просто взглянув на их объявления.
  3. Точная связь между свойствами и условиями несколько скрыта.

С безопасностью во время компиляции тоже

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

public interface IConditionalPropertySource<T>
{
    bool IsPropertyApplicable(Expression<Func<T, object>> expr);
}

Вы можете вызвать вышеуказанное как IsPropertyApplicable(o => o.SomeSetting) и получить "SomeSetting" в виде строки во время выполнения с помощью ((MemberExpression)expr.Body).Member.Name. Однако на самом деле мы не хотим работать с голой строкой в ​​любое время, потому что это будет означать, что проблема № 1, описанная выше, все еще существует.

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

class Test : IConditionalPropertySource<Test>
{
    // Your properties here:
    public string SomeSetting { get; set; }

    // This is the equality comparer used for the dictionary below
    private class MemberNameComparer :
        IEqualityComparer<Expression<Func<Test, object>>>
    {
        public bool Equals(
            Expression<Func<Test, object>> lhs, 
            Expression<Func<Test, object>> rhs)
        {
            return GetMemberName(lhs).Equals(GetMemberName(rhs));
        }

        public int GetHashCode(Expression<Func<Test, object>> expr)
        {
            return GetMemberName(expr).GetHashCode();
        }

        private string GetMemberName(Expression<Func<Test, object>> expr)
        {
            return ((MemberExpression)expr.Body).Member.Name;
        }
    }

    // A dictionary that maps member access expressions to boolean functions
    private readonly IDictionary<Expression<Func<Test, object>>, Func<bool>> 
        conditions = new Dictionary<Expression<Func<Test, object>>, Func<bool>>
        (new MemberNameComparer())
        {
            // The "SomeSetting" property is only visible on Wednesdays
            { 
                self => self.SomeSetting, 
                () => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday
            }
        };


    // This implementation is now trivial
    public bool IsPropertyApplicable(Expression<Func<Test, object>> expr)
    {
        return conditions[expr]();
    }
}

Это устраняет проблему № 1 (вы больше не можете ошибаться в именах свойств, компилятор это уловит) и улучшает № 3 (свойства и условия несколько более заметны). Он по-прежнему оставляет проблему № 2 нерешенной: вы не можете сказать, является ли SomeProperty условно видимым, просто взглянув на его объявление.

Однако вы можете расширить код, чтобы применить это во время выполнения:

  • Украсьте условно видимые свойства пользовательским атрибутом
  • Внутри конструктора перечислите все свойства класса, украшенные этим атрибутом, и все имена свойств, которые могут быть получены из ключей словаря.
  • Рассматривать обе перечисленные коллекции как наборы
  • Если наборы не равны, существует несоответствие между декорированными свойствами и свойствами, для которых определена логика условной видимости; бросить исключение
person Jon    schedule 05.03.2014
comment
Да, атрибуты со строками не годятся, это тоже была моя первая идея. Синтаксический анализ не является проблемой (я мог бы использовать CodeDom), но проверка во время компиляции и ремонтопригодность неудобны. Ваша идея с методом проверки условий на самом деле тоже натолкнула меня на идею. Что, если я создам метод с именем SomeSetting1Hide() (или, скорее, другое, частное свойство только с геттером???) для встраивания условия? Я мог бы объявить их вместе, так что это будет довольно хорошо видно! А Expression часть со словарями мне кажется слишком уж централизованным определением (придется искать его каждый раз, когда что-то меняю). - person Sinatr; 05.03.2014
comment
Я имею в виду наличие таких свойств, как bool SomeSetting1Hide { get { return SomeProperty2 > 100 && SomeProperty3 == 0; } }, которые будут вызываться перед проверкой необходимости добавления SomeSetting1 в список свойств, показанный на PropertyGrid. Не могли бы вы развить эту идею дальше? =П - person Sinatr; 05.03.2014
comment
@Sinatr: Ну, вы можете взять базовую версию (со строковым аргументом) и просто заставить ее найти и вызвать метод Hide с отражением, например return (bool)this.GetType().GetMethod(propertyName + "Hide").Invoke(this, null). Но мне это не очень нравится. - person Jon; 05.03.2014

Как уже указывал @Jon, использование атрибутов - неправильный путь.

В моем случае наиболее удовлетворительным решением на данный момент является объявление другого свойства с суффиксом Hide, которое содержит код проверки условия и используется посредством отражения в ConfigWrapper для проверки добавления этого параметра или нет:

public SomeType SomeSetting1 { get; set; }

public SomeType SomeSetting2 { get; set; }
protected SomeType SomeSetting2Hide { get { return SomeSetting3 = 4 && SomeSettings4 < 100; } }

Этот параметр должен быть объявлен как protected (сначала я сделал глупую ошибку), чтобы скрыть его от общедоступных настроек.

И затем в оболочке конфигурации:

    public ConfigWrapper(object obj)
    {
        _original = obj;
        // copy all properites
        foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(obj))
        {
            // filter hideable attributes
            bool add = true;
            foreach (Attribute attribute in property.Attributes)
                if (attribute is Common.IHide && (attribute as Common.IHide).Hide)
                {
                    add = false;
                    break;
                }

            ////////////////////////

            // filter configurable via hide property properties
            var hide = obj.GetType().GetProperty(property.Name + "Hide", BindingFlags.Instance | BindingFlags.NonPublic);
            if (hide != null && (bool)hide.GetValue(obj, null))
                add = false;

            ///////////////////////

            // add
            if (add)
                _collection.Add(new ConfigDescriptor(property));
        }
    }
person Sinatr    schedule 10.03.2014
comment
Вот как работает XmlSerializer. Я не уверен, что это относится ко всем свойствам, но я думаю, что да. Когда XmlSerializer встречает свойство, он проверяет наличие свойства с тем же именем и постфиксом Specified. Если это свойство возвращает false, XmlSerializer фактически проигнорирует это свойство. Я понял это при использовании генератора кода xsd, который генерирует такие Specified свойства для структур, допускающих значение NULL. - person Noel Widmer; 11.12.2016

Вы можете посмотреть на этот фреймворк:

http://www.codeproject.com/Articles/415070/Dynamic-Type-Description-Framework-for-PropertyGri

У этого есть много динамических featuers.

person Mizan    schedule 20.06.2014
comment
Пожалуйста, не публикуйте ответы только по ссылке - person Phantômaxx; 20.06.2014
comment
Добро пожаловать в Stack Overflow! Ответы только по ссылке, хотя они могут дать ответ на вопрос, могут стать недействительными, если страница изменится: предоставьте более содержательный ответ с информацией из вашей ссылки. - person AstroCB; 20.06.2014