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

Възможно ли е да се посочи постигане на следното:

[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
AFAIK, не можете да направите това по време на компилиране. Не можете условно да прилагате атрибути въз основа на стойности по време на изпълнение.   -  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() (или по-скоро друго, частно свойство само с getter???), в което да вградя условие? Мога да ги декларирам заедно, така че ще се вижда доста добре! И Expression частта с речниците ми изглежда твърде централизирана дефиниция (ще трябва да я търся всеки път, когато променям нещо). - person Sinatr; 05.03.2014
comment
Това, което имам предвид, е да има свойства като bool SomeSetting1Hide { get { return SomeProperty2 > 100 && SomeProperty3 == 0; } }, които ще бъдат извикани преди проверка дали SomeSetting1 трябва да се добави към списък със свойства, показан в PropertyGrid. Можете ли да развиете тази идея по-подробно? =P - 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 срещне свойство, той проверява за свойство със същото име и postfix Specified. Ако това свойство върне false, XmlSerializer всъщност ще игнорира свойството. Разбрах това, когато използвах генератора на код xsd, който генерира такива Специфицирани свойства за нулеви структури. - person Noel Widmer; 11.12.2016

Можете да разгледате тази рамка:

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

Това има много динамични функции.

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