Why does WPF seem to bypass TypeDescriptionProviderAttribute when INotifyPropertyChanged is implemented? - wpf

I'm trying to use the [TypeDescriptionProviderAttribute] in order to give my class a custom type descriptor. This works, but when I implement INotifyPropertyChanged WPF seems to ignore the custom type descriptor and go straight for the CLR property (if it exists). Here's a snippet, I'll paste the full example later on:
//[TypeDescriptionProvider(typeof(MyProvider))]
class MyModel : Object
//, INotifyPropertyChanged
//, ICustomTypeDescriptor
{
public string TheProperty { get { return "CLR - TheProperty"; } }
I bind a TextBlock to TheProperty. When I...
Leave everything commented
I see "CLR - TheProperty" as expected.
Use [TypeDescriptionProvider]
I see "MyPropertyDescriptor - TheProperty" as expected.
Use ICustomTypeDescriptor
I see "MyPropertyDescriptor - TheProperty" as expected.
Use ICustomTypeDescriptor and INotifyPropertyChanged
I see "MyPropertyDescriptor - TheProperty" as expected.
Use [TypeDescriptionProvider] and INotifyPropertyChanged
I see "CLR - TheProperty". Why is this? The weird thing is that custom properties without a CLR property are shown normally. My custom type descriptor also returns a "MyPropertyDescriptor - AnotherProperty" which works in all cases because there is no CLR AnotherProperty defined.
In summary, given this XAML
<StackPanel>
<TextBlock Text="{Binding TheProperty}" />
<TextBlock Text="{Binding AnotherProperty}" />
</StackPanel>
AnotherProperty always works as expected because the model does not have a CLR property named "AnotherProperty". TheProperty works as expected except when [TypeDescriptionProvider] and INotifyPropertyChanged are both used.
Here's the full code. It's a bit long but most of it is irrelevant, it's just required by System.ComponentModel
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
DataContext = new MyModel();
}
}
//[TypeDescriptionProvider(typeof(MyProvider))]
class MyModel : Object
//, INotifyPropertyChanged
//, ICustomTypeDescriptor
{
public string TheProperty { get { return "CLR - TheProperty"; } }
public event PropertyChangedEventHandler PropertyChanged;
public AttributeCollection GetAttributes()
{
return TypeDescriptor.GetAttributes(this);
}
public string GetClassName()
{
return TypeDescriptor.GetClassName(this);
}
public string GetComponentName()
{
return TypeDescriptor.GetComponentName(this);
}
public TypeConverter GetConverter()
{
return TypeDescriptor.GetConverter(this);
}
public EventDescriptor GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(this);
}
public PropertyDescriptor GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(this);
}
public object GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(this, editorBaseType);
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(this, attributes);
}
public EventDescriptorCollection GetEvents()
{
return TypeDescriptor.GetEvents(this);
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return TypeDescriptor.GetProperties(this, attributes);
}
public PropertyDescriptorCollection GetProperties()
{
return MyTypeDescriptor.GetCustomProperties();
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
}
class MyProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new MyTypeDescriptor();
}
}
class MyTypeDescriptor : CustomTypeDescriptor
{
public override PropertyDescriptorCollection GetProperties()
{
return GetCustomProperties();
}
public static PropertyDescriptorCollection GetCustomProperties()
{
return new PropertyDescriptorCollection(
new[] {
new MyPropertyDescriptor("TheProperty"),
new MyPropertyDescriptor("AnotherProperty")
});
}
}
class MyPropertyDescriptor : PropertyDescriptor
{
public MyPropertyDescriptor(string propName)
: base(propName, null)
{
}
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { return typeof(MyModel); }
}
public override object GetValue(object component)
{
return "MyPropertyDescriptor - " + Name;
}
public override bool IsReadOnly
{
get { return true; }
}
public override Type PropertyType
{
get { return typeof(string); }
}
public override void ResetValue(object component)
{
throw new InvalidOperationException("cannot reset value");
}
public override void SetValue(object component, object value)
{
throw new InvalidOperationException("property is readonly");
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
}

Old question, but for people looking for an answer..
Problem is in System.Windows.PropertyPath.ResolvePropertyName(String, Object, Type, Object, Boolean) private method. I have found it in PresentationFramework.dll in .NET 4.0.
Extracted from .NET Reflector:
object propertyHelper = DependencyProperty.FromName(str, ownerType);
if ((propertyHelper == null) && (item is ICustomTypeDescriptor))
{
propertyHelper = TypeDescriptor.GetProperties(item)[str];
}
if ((propertyHelper == null) && ((item is INotifyPropertyChanged) || (item is DependencyObject)))
{
propertyHelper = this.GetPropertyHelper(ownerType, str);
}
if (propertyHelper == null)
{
propertyHelper = TypeDescriptor.GetProperties(item)[str];
}
if (propertyHelper == null)
{
propertyHelper = this.GetPropertyHelper(ownerType, str);
}
if ((propertyHelper == null) && throwOnError)
{
throw new InvalidOperationException(SR.Get("PropertyPathNoProperty", new object[] { ownerType.Name, str }));
}
return propertyHelper;
As you can see, retrieving property identifier (DependencyProperty / PropertyDescriptor / PropertyInfo) goes like this:
Try get DependencyProperty,
If item implements ICustomTypeDescriptor, use TypeDescriptor to get PropertyDescriptor,
If item implements INotifyPropertyChanged or is DependencyObject, use System.Reflection to get PropertyInfo,
Else use TypeDescriptor to get PropertyDescriptor,
Else use System.Reflection to get PropertyInfo,
Else throw exception or return null.
So System.Reflection/PropertyInfo gets precedence over TypeDescriptor/PropertyDescriptor if item implements INotifyPropertyChanged interface. I believe they choose this strategy for performance reasons because PropertyInfo is much more lighter than PropertyDescriptor.
Solution to your problem would be to implement ICustomTypeDescriptor (preferably explicitly) so that it transfers ICustomTypeDescriptor method calls to appropriate TypeDescriptor method calls but not with object parameter, but Type parameter (this.GetType()). This way your TypeDescriptionProvider will be used.

Related

WPF designer properties window: property does not show nested properties

I am trying to edit a complex property of a WPF custom UserControl in the properties windows. But, the nested properties of the property are not always expandable. In the verbose form (don't know the corrent name):
<local:PointControl>
<local:PointControl.Point>
<local:Point X="3" Y="4"/>
</local:PointControl.Point>
</local:PointControl>
I can edit the nested properties:
In the short form, provided by a TypeConverter:
<local:PointControl Point="0,0"/>
I cannot edit the nested properties:
The code is as follows:
Point
namespace WpfApplication1
{
[TypeConverter(typeof(PointConverter))]
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point() { }
public Point(int x, int y)
{
X = x;
Y = y;
}
public static bool CanParse(string value); // not important
public static Point Parse(string value); // not important
public override string ToString()
{
return string.Format("{0},{1}",X,Y);
}
}
}
PointConverter
namespace WpfApplication1
{
public class PointConverter : TypeConverter
{
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is Point)
{
return (value as Point).ToString();
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); ;
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
var point = Point.Parse(value as string);
return point;
}
return base.ConvertFrom(context, culture, value);
}
public override bool IsValid(ITypeDescriptorContext context, object value)
{
if (!(value is string))
{
return false;
}
return Point.CanParse(value as string);
}
}
}
UserControl
namespace WpfApplication1
{
public partial class PointControl : UserControl
{
public Point Point
{
get;set;
}
public PointControl()
{
InitializeComponent();
}
}
}
UPDATE
When I add an inline value editor it won't update the XAML... :(.
xaml:
<local:PointControl Point="2,2">
MetaData.cs
[assembly: ProvideMetadata(typeof(WpfApplication1.Design.Metadata))]
namespace WpfApplication1.Design
{
internal class Metadata : IProvideAttributeTable
{
public AttributeTable AttributeTable
{
get
{
AttributeTableBuilder builder = new AttributeTableBuilder();
builder.AddCustomAttributes(typeof(WpfApplication1.PointControl), nameof(WpfApplication1.PointControl.Point), PropertyValueEditor.CreateEditorAttribute(typeof(PointerValueEditor)));
return builder.CreateTable();
}
}
}
}
PointerValueEditor
namespace WpfApplication1.Design
{
class PointerValueEditor : PropertyValueEditor
{
public PointerValueEditor()
{
var stringReader = new System.IO.StringReader(
#"<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<StackPanel>
<TextBox Text=""{Binding Value.X, Mode=TwoWay, UpdateSourceTrigger=LostFocus}""/>
<TextBox Text=""{Binding Value.Y, Mode=TwoWay, UpdateSourceTrigger=LostFocus}""/>
</StackPanel>
</DataTemplate>");
var xmlReader = System.Xml.XmlReader.Create(stringReader);
var dataTemplate = System.Windows.Markup.XamlReader.Load(xmlReader) as DataTemplate;
this.InlineEditorTemplate = dataTemplate;
}
}
}
The Code is based on https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2013/ayybcxe5(v=vs.120)
You will want to implment an inline value editor

Winforms Designer custom property does not keep its value when I build

I have a control that I subclass from Label (as an example) and add a custom property to it. I have a custom editor that allows me to pick an object of a custom type. The custom type is attributed with the converter and and editor. Editing works fine - I click in my property cell in the VS designer property grid and the ellipsis displays. I click that and I get my custom form displaying my list to choose from. I make a selection and that selection shows up in my property grid. All good. However, I can not seem to clear that value by backspacing over it like I can with most properties. And more importantly, as soon as I build the project the property value disappears. Before I build, I can go from control to control and the value is set properly, but as soon as I build (or save and close the form) the property loses its value. My custom control looks like this:
public class MyLabel : Label
{
private MyAlias _alias;
public MyAlias Alias
{
get
{
return _alias;
}
set
{
if (_alias != value)
{
_alias = value;
}
}
}
}
The custom type:
[System.ComponentModel.TypeConverter(typeof(MyConverter))]
[System.ComponentModel.Editor(MyTypeEditor, System.Drawing.Design.UITypeEditor)]
public class MyAlias
{
private string _aliasName;
public string AliasName
{
get
{
return _aliasName;
}
set
{
if (_aliasName != value)
{
_aliasName = value;
}
}
}
public MyAlias(string aliasName)
{
if (aliasName == null)
{
return;
}
_aliasName = aliasName;
}
public override string ToString()
{
return _aliasName;
}
}
Custom converter:
public class MyConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (!(value is string))
{
return base.ConvertFrom(context, culture, value);
}
return new MyAlias((string)value);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
MyAlias alias = (MyAlias)value;
return alias.ToString();
}
}
Custom Editor:
public partial class AliasEditorForm : Form
{
private object _value;
public object Value
{
get
{
return _value;
}
set
{
if (_value != value)
{
_value = value;
}
}
}
public AliasEditorForm()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
// Button does have DialogResult property set to OK
_value = new MyAlias(...text string from form controls here...);
this.Close();
}
}
public class MyTypeEditor : UITypeEditor
{
protected IWindowsFormsEditorService edSvc = null;
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if (null != context && null != context.Instance && null != provider)
{
edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (null != edSvc)
{
AliasEditorForm form = new AliasEditorForm();
form.Value = value;
DialogResult r = edSvc.ShowDialog(form);
return (DialogResult.OK == r) ? form.Value : value;
}
}
return value;
}
}
It's been a long time since I've worked in WinForms and I never really did much dabbling with design-time stuff so forgive me if I've done something terribly stupid. If I didn't provide enough info, let me know. Thanks in advance!
EDIT: I notice it is not generating the code for the property in the form.Designer.cs file. Still not sure why...
EDIT: Changed the code for the type converter. I can now clear the property by backspacing. I still can't get the designer to generate the code to preserve the property value.
Dennis

WPF Propertygrid with custom sorting

I'm looking for a PropertyGrid for my WPF project that allows me to customize the ordering the properties / categories are listed. Right now I'm using Extended WPF Toolkits (Community Edition) PropertyGrid with CustomPropertyDescriptors. My researches showed, that it's not possible to have custom sorting with that PropertyGrid.
Is there a (preferably free) solution?
Ordering of properties in the Extended WPF Toolkit can be achieved by decorating the property with the PropertyOrderAttribute attribute.
If you don't want to pollute POCO's by decorating them with attributes at design time, or the order is dynamic in some way, then it's possible to add the attribute at run time by creating a type converter and overriding the GetProperties method. For example, if you wish to maintain the index order of a generic IList type:
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;
public class MyExpandableIListConverter<T> : ExpandableObjectConverter
{
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
if (value is IList<T>)
{
IList<T> list = value as IList<T>;
PropertyDescriptorCollection propDescriptions = new PropertyDescriptorCollection(null);
IEnumerator enumerator = list.GetEnumerator();
int counter = -1;
while (enumerator.MoveNext())
{
counter++;
propDescriptions.Add(new ListItemPropertyDescriptor<T>(list, counter));
}
return propDescriptions;
}
else
{
return base.GetProperties(context, value, attributes);
}
}
}
With the ListItemPropertyDescriptor being defined as follows:
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using System.ComponentModel;
public class ListItemPropertyDescriptor<T> : PropertyDescriptor
{
private readonly IList<T> owner;
private readonly int index;
public ListItemPropertyDescriptor(IList<T> owner, int index) : base("["+ index+"]", null)
{
this.owner = owner;
this.index = index;
}
public override AttributeCollection Attributes
{
get
{
var attributes = TypeDescriptor.GetAttributes(GetValue(null), false);
//If the Xceed expandable object attribute is not applied then apply it
if (!attributes.OfType<ExpandableObjectAttribute>().Any())
{
attributes = AddAttribute(new ExpandableObjectAttribute(), attributes);
}
//set the xceed order attribute
attributes = AddAttribute(new PropertyOrderAttribute(index), attributes);
return attributes;
}
}
private AttributeCollection AddAttribute(Attribute newAttribute, AttributeCollection oldAttributes)
{
Attribute[] newAttributes = new Attribute[oldAttributes.Count + 1];
oldAttributes.CopyTo(newAttributes, 1);
newAttributes[0] = newAttribute;
return new AttributeCollection(newAttributes);
}
public override bool CanResetValue(object component)
{
return false;
}
public override object GetValue(object component)
{
return Value;
}
private T Value
=> owner[index];
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override void SetValue(object component, object value)
{
owner[index] = (T)value;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override Type ComponentType
=> owner.GetType();
public override bool IsReadOnly
=> false;
public override Type PropertyType
=> Value?.GetType();
}
Portions of this code were adapted from the following SO answer

Disable button if validation in model has error

Hi I make validation on error in my model class.
public class CurrentUser:IDataErrorInfo, INotifyPropertyChanged
{
//...
private string _validationResult;
private string _nick;
public string Nick
{
get { return _nick; }
set
{
_nick = value;
NotifyPropertyChanged("Nick");
}
}
public string ValidationResult
{
get { return _validationResult; }
private set
{
_validationResult = value;
NotifyPropertyChanged("ValidationResult");
}
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
#region Implementation of IDataErrorInfo
private string NickValid()
{
if (string.IsNullOrEmpty(Nick))
{
return NickNull;
}
if (Regex.IsMatch(Nick, "[^a-zA-Z0-9-_.]"))
{
return NickInvalidCharacters;
}
return string.Empty;
}
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string propertyName]
{
get
{
ValidationResult = string.Empty;
switch (propertyName)
{
case "Nick":
ValidationResult = NickValid();
break;
default:
break;
}
return ValidationResult;
}
}
#endregion
}
This model class I use in view model and I bind Nick property of model class to the Text property of comboBox control.
Also I bind method LogOn from view model class on button click event in view. I would like disabale button if validation in model class has error:
View model:
[Export(typeof(ILogOnViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class LogOnViewModel : Screen, ILogOnViewModel,
IPartImportsSatisfiedNotification
{
public CurrentUser CurrentUser { get; set; }
public bool CanLogOn
{
get
{
return string.IsNullOrWhiteSpace(CurrentUser.ValidationResult);
}
}
//bind on button click event
public void LogOn()
{}
}
Solution is simple set CanLogOn property on false if validation in CurrentUser (object) property has error.
But I don’t how notify property CanLogOn that in model class is not error. I run app and button is still disabled.
I need achive this behavior in model:
public string ValidationResult
{
get { return _validationResult; }
private set
{
_validationResult = value;
NotifyPropertyChanged("ValidationResult");
//notify property CanLogOn in view model class
}
}
Any advice? Thank.
Attach an event handler to the PropertyChanged event of the user in your viewmodel:
CurrentUser.PropertyChanged += new PropertyChangedEventHandler(CurrentUser_PropertyChanged);
Add send a notification if the ValidationResult changes:
void CurrentUser_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "ValidationResult") NotifyPropertyChanged("CanLogOn");
}
Note: If your reference CurrentUser is overwritten you need to add the event handler to the new object. You could do this by placing the attachment code in the setter of CurrentUser.

WPF - Lock Position and size on a canvas

I have a canvas that I can create and drop elements onto. I can move them and size them with a thumb or through binding to a property grid.
Now, I need to implement a feature to "lock" position and size. I started with try and set min and max height\width to the actual height\width. The initial effort wasn't so great. And I still don't have a strategy for fixing the location.
So before I go off and wade through 25 approaches that don't work, perhaps someone has a suggestion. I'd much appreciate it.
Thanks,
jeff
You could just disable the Thumb and the PropertyGrid...
Of course, for the PropertyGrid it is not ideal... it would be better to keep it enabled, but read-only, unfortunately the PropertyGrid doesn't have a ReadOnly property... A possible solution would be to wrap your object in a custom type descriptor which would present the properties as read-only. Here are two classes to achieve that (sorry for the long code...) :
ReadOnlyTypeDescriptor :
public class ReadOnlyTypeDescriptor : ICustomTypeDescriptor
{
public ReadOnlyTypeDescriptor(object target)
{
TypeDescriptionProvider provider = TypeDescriptor.GetProvider(target);
_originalDescriptor = provider.GetTypeDescriptor(target);
}
public ReadOnlyTypeDescriptor(ICustomTypeDescriptor descriptor)
{
_originalDescriptor = descriptor;
}
private ICustomTypeDescriptor _originalDescriptor;
private PropertyDescriptor MakeReadOnly(PropertyDescriptor propertyDescriptor)
{
return new ReadOnlyPropertyDescriptor(propertyDescriptor);
}
private PropertyDescriptorCollection MakeReadOnly(PropertyDescriptorCollection propertyDescriptors)
{
var descriptors = propertyDescriptors
.Cast<PropertyDescriptor>()
.Select(pd => new ReadOnlyPropertyDescriptor(pd))
.ToArray();
return new PropertyDescriptorCollection(descriptors, true);
}
#region ICustomTypeDescriptor Members
public AttributeCollection GetAttributes()
{
return _originalDescriptor.GetAttributes();
}
public string GetClassName()
{
return _originalDescriptor.GetClassName();
}
public string GetComponentName()
{
return _originalDescriptor.GetComponentName();
}
public TypeConverter GetConverter()
{
return _originalDescriptor.GetConverter();
}
public EventDescriptor GetDefaultEvent()
{
return _originalDescriptor.GetDefaultEvent();
}
public PropertyDescriptor GetDefaultProperty()
{
return MakeReadOnly(_originalDescriptor.GetDefaultProperty());
}
public object GetEditor(Type editorBaseType)
{
return _originalDescriptor.GetEditor(editorBaseType);
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return _originalDescriptor.GetEvents(attributes);
}
public EventDescriptorCollection GetEvents()
{
return _originalDescriptor.GetEvents();
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return MakeReadOnly(_originalDescriptor.GetProperties(attributes));
}
public PropertyDescriptorCollection GetProperties()
{
return MakeReadOnly(_originalDescriptor.GetProperties());
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return _originalDescriptor.GetPropertyOwner(pd);
}
#endregion
}
ReadOnlyPropertyDescriptor :
public class ReadOnlyPropertyDescriptor : PropertyDescriptor
{
public ReadOnlyPropertyDescriptor(PropertyDescriptor descriptor)
: base(
descriptor.Name,
descriptor.Attributes.Cast<Attribute>().ToArray())
{
_originalDescriptor = descriptor;
}
private PropertyDescriptor _originalDescriptor;
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { return _originalDescriptor.ComponentType; }
}
public override object GetValue(object component)
{
return _originalDescriptor.GetValue(component);
}
public override bool IsReadOnly
{
get { return true; }
}
public override Type PropertyType
{
get { return _originalDescriptor.PropertyType; }
}
public override void ResetValue(object component)
{
throw new NotSupportedException();
}
public override void SetValue(object component, object value)
{
throw new NotSupportedException();
}
public override bool ShouldSerializeValue(object component)
{
return _originalDescriptor.ShouldSerializeValue(component);
}
}
To show the object target as read-only in the PropertyGrid, just do that :
propertyGrid.SelectedObject = new ReadOnlyTypeDescriptor(target);
It will show the same properties, but they won't be editable...
OK, this solution is probably a little overkill for your needs... but I think it can be handy in some cases ;)

Resources