I'm looking for a way to set up my own default property values for different types of controls in my C# .NET windows application. The default property values should 'override' the existing default values of the controls, but still be 'overridable' by setting the property values explicitly in the designer.
This is to simplify the process of changing default appearance/behaviour of controls when the client (or myself) change their mind for the 10th time. This relates especially to controls like the DataGridView or 3rd party controls where there are tons of layout-related properties to maintain.
I am aware of the ability to create inherited controls and use the DefaultValue attribute, but this is not the solution I'm looking for for a couple of reasons:
It's a hassle having to inherit of every type of control I want to specify custom properties for, not to mention overriding/shadowing the properties and setting the DefaultValue attribute.
I can no longer use the standard .NET controls, but have to use the inherited controls.
The number of inherited controls increases over time and clutters up the toolbox.
Myself or other developers on the project forget to use the new inhertied types in times of haste, resulting in inconsitent behaviour/appearance of controls.
This is how I imagined that it will work:
Example 1: A DataGridView by default has background color
SystemColors.Window. I set my own
default value to Color.Blue (how
outrageous!). In the designer, the
default background color is used,
i.e. the background color is not set
explicitly in the .designer.cs file.
When running the application, a portion of code is executed, causing the grid to turn
blue, as specified by me.
Example 2: The background color of the same DataGridView is set to
Color.Red in the designer. This
overrides my own default value of blue, showing a red background
in the grid, both in design-time and run-time.
Solution
The solution for me was to use reflection to check the DefaultValue attribute, as suggested by Daniel Brückner.
I recurse through all controls on a form, calling SetDefaultValues for each control. For each property value to set, I call the SetValue method, which makes sure only properties that haven't been changed from their default values, are set.
There is one flaw in this approach, though. Properties that have been set explicitly in the designer, but do not differ from their default values, will be overwritten by the SetValue method.
void SetDefaultValues(Control control)
{
if (control is DataGridView)
{
SetValue(control, "BackColor", Color.Blue);
}
else if (control is TextBox)
{
// etc.
}
}
private static void SetValue(object control, string propertyName, object newValue)
{
System.Reflection.PropertyInfo prop = control.GetType().GetProperty(propertyName);
if (prop == null)
{
throw new ArgumentException(string.Format(
"Specified property \"{0}\" does not exist on type \"{1}\".", prop.Name, control.GetType().FullName),
"propertyName");
}
bool defaultValueFound = false;
object defaultValue = null;
foreach (object attr in prop.GetCustomAttributes(true))
{
if (attr is DefaultValueAttribute)
{
defaultValue = ((DefaultValueAttribute)attr).Value;
defaultValueFound = true;
break;
}
}
if (!defaultValueFound && prop.PropertyType.IsValueType)
{
// Get default value for value types if no default value was specified by attributes:
defaultValue = Activator.CreateInstance(prop.PropertyType);
}
if (defaultValue == null || defaultValue.Equals(prop.GetValue(control, null)))
{
// If default value matches current value, set new value:
prop.SetValue(control, newValue, null);
}
}
While not as pretty as generics, you might me able to do something with Control Builders to pull this off.
Edit:
Last night I did a quick prototype of a generic wrapper control with the ControlBuilder. I am not happy with the results. While you can probably get it to work, I beleive a new Page or Container class might be a much simpler result. The source code I used in my test is avalible on my blog.
There are several solutions I have used or I can think of.
Inheriting the control, but you mentioned that already.
Some more advanced control libraries (like DevExpress) have the build-in ability to load the layout from configuration files (XML in the case of DevExpress) or are even completly skinnable (true for DevExpress, too).
Sometimes I create extension method for the controls and call them in the constructor of the user control or form. This is an easy way to enable or disable sets of functionalities like sorting. multiselect, or column reordering in data grids and gives a consistent behavior and look.
Use data binding and bind the properties to some configuration data. I believe there is even a build in functionality - user settings or something like that - but I have never used this feature.
Calling the extension method on all controls like proposed above is not very handy in larger projects. You could recursivly visit all controls in a form when it is created, look at the properties, compare to the default value (using reflection to get the DefaultValue attribute), and if they don't match (that is the value has been overriden in the designer) load your default value from some file or in-memory store and apply it.
you could overide page instead and have a loop through all the controls e.g.
foreach (Control c in Page.Controls)
{
if (c is Textbox)
{
(Textbox)c.Color.blah.blah.blah ;)
}
///etc
Recurse through (c.Controls);
}
Related
Given a Class of X that has multiple properties that are bindable, how do I determine which one should be the default property to select via reflection?
In the Winforms designer you can select Databinding. How does Visual Studio determine that "EditValue" should be the default property to bind to rather than say "Text"?
I already know how to get properties and attributes from the object, but I'm missing something that would tell me which one to use as default.
You can rely on DefaultBindingProperty attribute of the class.
For example a DateTimePicker is decorated with [DefaultBindingProperty("Value")] but a ComboBox is decorated with [DefaultBindingProperty("Text")].
You can create a function like following, to get name of the default binding property of a control:
public string GetDefaultBindingPropertyValue(Control c)
{
var att = c.GetType().GetCustomAttributes(true)
.OfType<DefaultBindingProperty>().FirstOrDefault();
return att?.Name;
}
Side Note
You may be interested to these attributes as well for some complex scenarios:
LookupBindingProperties: Specifies the properties that support lookup-based binding. List controls like ComboBox and ListBox are decorated by this attribute, [LookupBindingProperties("DataSource", "DisplayMember", "ValueMember", "SelectedValue")].
ComplexBindingProperties: Specifies the data source and data member properties for a component that supports complex data binding. DataGridView has been decorated by this attribute, [ComplexBindingProperties("DataSource", "DataMember")].
Context
I've been working on a custom collection editor / designer for a custom ASP.Net web control. The web control exposes a strange hierarchy, so a custom editor seemed like the right thing to do to make it easier for developers.
Building ASPX code and using the web control works. In other words, things like PersistChildren and ParseChildren are taken care of.
The signature of the property in the web control looks something like this:
[PersistenceMode(PersistenceMode.InnerProperty)]
[Themeable(false)]
[Browsable(false)]
public virtual DimensionsCollection Dimensions { get; internal set; }
Note that the property is not public; if it were public, all kinds of things in the designer will go wrong. DimensionsCollection is a class that simply inherits List<Dimension>. The Dimension class itself is nothing fancy, just a thing with some properties.
Just because I think it looks cool, I want to be able to modify the property from an action in the designer. To do that, I implemented a ControlDesigner class and added an ActionList. One of the actions there is a linkbutton that opens an editor:
var editor = new Editors.DimensionEditor(control.Dimensions);
if (editor.ShowDialog() == DialogResult.OK)
{ /* SEE BELOW */ }
The editor itself is a windows form that takes a List<Dimension> as constructor argument and modifies the collection.
Problem
When I use this code, I can see that the editor works and that the control collection is updated in the 'designer' view. If I open the editor multiple times, the state changes, meaning that somewhere in memory the state is updated by the editor.
However, if I go to the ASPX code, I can see that the Dimensions are not there anymore. So, the problem in a nutshell is that I somehow have to tell Visual Studio to write/serialize/persist the property to the ASPX file. (simple as that...)
Strangely, I cannot find anywhere how to do this... even though a normal CollectionEditor seems to be capable of doing just that (which I cannot subclass unfortunately)
Some things I tried
For other properties I noticed you have to use something like this, but this doesn't seem to work. Code was entered at the point marked as 'see below' or in some cases to a helper call in the designer called from that point:
PropertyDescriptor pd = TypeDescriptor.GetProperties(base.Component)["Dimensions"];
// use setter with internal property -> no effect
// this.OnComponentChanged(this, new ComponentChangedEventArgs(this.Component, pd, null, newdim)); -> no effect
// use getter to obtain list -> populate that using another list that's created in the editor
I can understand why it doesn't work; apparently someone has to tell Visual Studio that the property has changed... I just don't know how to do just that.
This was really a pain to figure out with apparently no sources online that explain how to do this.
Basically you want to use the OnComponentChanging / Changed methods to notify the designer. And apparently the designer uses transactions for the rest of the logic. (My guess is that it has to do with undo/redo behavior). For a normal type this is done automatically when you use the PropertyDescriptor, for collections it apparently doesn't wrap the collection which means you have to do it manually.
To solve the issue, you need to create a small method like this in either the UITypeEditor or in the DesignerActionList class your implementing:
private void ChangeAction(List<Dimension> newDimensions)
{
IDesignerHost host = GetService(typeof(IDesignerHost)) as IDesignerHost;
PropertyDescriptor pd = TypeDescriptor.GetProperties(typeof(MyControl))["Dimensions"];
var dimensions = (DimensionsCollection)pd.GetValue(control);
var trans = host.CreateTransaction();
IComponentChangeService ccs = (IComponentChangeService)GetService(typeof(IComponentChangeService));
ccs.OnComponentChanging(control, pd);
dimensions.Clear();
dimensions.AddRange(newDimensions);
ccs.OnComponentChanged(control, pd, null, dimensions);
trans.Commit();
}
If you're implementing a UITypeEditor, make sure to use context.Instance from EditValue as the control and the given provider to lookup the services.
In the project I have some custom WebUserControls for form elements (they encapsulate some standard validators and other system specific functions). My user controls are "DropDownListField" and "TextBoxField". In the code behind of a page I have this code:
string parameterValue = null;
foreach (object control in myMultiView.Views[myMultiView.ActiveViewIndex].Controls)
{
if (control.GetType() == typeof(DropDownListField))
parameterValue = ((DropDownListField)control).Value;
if (control.GetType() == typeof(TextBoxField))
parameterValue = ((TextBoxField)control).Value;
}
For some reason the "if" statements always return false even when I step through the code and see that "control" is getting assigned my web user control. This code is in another place in the project exactly the same except in the other location the standard .net controls "TextBox" and "DropDownList" are used and in the other location the code works.
Does anybody know why this wouldn't work with web user controls?
UPDATE:
Hmm so in debugging I found this:
?control.GetType();
BaseType: {Name = "DropDownListField" FullName = "WebUI.UserControls.Fields.DropDownListField"}
?typeof(DropDownListField);
BaseType: {Name = "UserControl" FullName = "System.Web.UI.UserControl"}
So typeof is just recognizing they are user controls not the full type it seems.
Does anybody know how I would check for a specific user control type?
I'm guessing they aren't the same type, use debugging to find out the actual type.
Also, try using the 'is' keyword instead.
PS: It might be cleaner for you to say if (control is DropDownListField)
I don't recall if a view directly includes its children in Controls, but I wouldn't be surprised if Controls contained only one element, which would be a container of some sorts. Therefore, your controls are potentially in Controls[0].Controls or even further down. I would advise you create a method that finds the child recursively.
Actually, your controls should all implement a common interface (example:
interface ICustomFieldWithValue { string Value {get; set; }}
). Your resulting code would be much cleaner.
c2.GetType().ToString() == "System.Web.UI.WebControls.Label"
I am working on a quiz control in asp.net with dynamically created questions and options.
The main control is basically a container to hold all of the questions.
In design view users can add questions through a custom Collection Editor.
Everytime i add a question to the collection editor list it generates a question tag for me.
Inside each question object is a label and a n amount of Option objects that inherit the Radiobutton Control. Each of these Option objects in turn represent a option the user can select for each question.
This all works except i am now at the part where i want to be able to read the Checked value of each radiobutton. When i want to implement this quiz inside a page and check the questions i want to put a button in this page and call the following function that is inside the control:
$
public String checkQuestions()
{
if (questions != null)
{
foreach (Question question in questions)
{
options = question.readOptions();
int i = 0;
foreach (Option option in options)
{
testLabel.Text = option.Checked.ToString(); // test purposes only
}
}
}
return errors;
}
However once i select a radiobutton and click on the submit button the Checked value will always turn out false for all of the options.
Basically it is losing its checked value after a Postback and i am just stuck in trying to solve it.
Would appreciate it if anyone could point me in the right direction.
At a first glance, there are two things I'd check. Firstly, make sure you're implementing IPostBackDataHandler. this requires you to implement two methods, LoadPostData and RaisePostDataChangedEvent. At my first guess, the first one is probably the source of your problem.
Handling postback manually
LoadPostData takes a string postDataKey and a NameValueCollection postCollection and returns a bool indicating whether or not the value has changed as a result of the postback. You don't need to implement this the way .Net originally intends, for example I created a control that held several radio buttons (that for reasons that aren't important here couldn't simply be a RadioButtonList control) and so made sure they were all named by a property string GroupName and inspected the postCollection for that GroupName:
public bool LoadPostData(string postDataKey,
System.Collections.Specialized.NameValueCollection postCollection)
{
bool oldValue = _isChecked;
postCollection = HttpContext.Current.Request.Form; // See note below
_isChecked = (postCollection[this.GroupName] == this.Text);
return oldValue == _isChecked;
}
You'll notice that I'm redefining the postCollection here; this is because postCollection only contains a subset of the HttpRequest.Form corresponding to what ASP.Net thinks your control should care about. As you're also building a composite control here, you probably want to do the same.
Don't worry if this doesn't work first time round; it's worth stepping through what gets passed into this method in debug mode (or outputting things to the HttpContext.Trace, which I often find easier) to see why your code isn't quite what you need.
A quick caveat
One last thing: LoadPostData is only called if the posted form contains a field with a name which matches the UniqueID of your control. As your control is a composite control, you might want to cowboy this slightly, like so:
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
writer.WriteBeginTag("input");
writer.WriteAttribute("type", "hidden");
writer.WriteAttribute("name", this.UniqueID);
writer.WriteAttribute("value", "post");
writer.Write(" />");
}
It's a dirty hack, but it'll work ;o)
Handling viewstate manually
If handling the postback manually doesn't solve your problem, it might be that you need to mess with the viewstate of your control. Don't worry, this is nowhere near as scary as it seems, provided you follow a few simple rules.
To handle your viewstate manually, you just need to override two methods called, obviously enough, LoadViewState and SaveViewState. The first takes an object of viewstate to inflate and the other returns that same object structure. If you make your SaveViewState override return something containing the structure you need to save all the important properties that need persisting, then you just inflate it again in your LoadViewState method.
Here's where the first of the cunning tricks comes up. There are certain datatypes that you should use for saving viewstate and you should never use any other type (because other types are stored really inefficiently). The types that will probably be most useful to you are System.Web.UI.Pair, System.Web.UI.Triplet and our old friends System.Collections.ArrayList and System.Collections.Hashtable. Pairs and Triplets simply store two or three values of type object; ArrayLists are effectively a List<object>.
I'd guess that, in your circumstance, you probably want to store either (1) an ArrayList of boolean flags, storing the "checkedness" of your radiobuttons or (2) an ArrayList of strings or ints, storing the IDs or index of the checked radiobuttons.
In the control I mentioned earlier, I just needed to store the checkedness and the Text property, so my LoadViewState and SaveViewState methods looked like this:
protected override void LoadViewState(object savedState)
{
Pair state = savedState as Pair;
if (state != null)
{
_isChecked = state.First as Nullable<bool> ?? false;
this.Text = state.Second as string;
}
}
protected override object SaveViewState()
{
return new Pair(_isChecked, this.Text);
}
Again, if this doesn't work first time, you almost certainly want to step through the code or throw things into the Trace. Importantly, you probably want to avoid throwing Exceptions from these methods, in case your viewstate is corrupt or non-existent or something.
Further reading on viewstate
There are a couple of very useful articles I keep bookmarked for when I'm messing with viewstate. The first one explains about why you should only store certain types in the viewstate (like using ArrayList and Hashtable, rather than List<T> and Dictionary<TKey, TValue>) and the second is a good in-depth explanation of how all this viewstate stuff actually works.
Don't let the BinaryFormatter get at it!
Truly understanding ViewState
I hope all this helps resolve your problem.
I'm using a PropertyGrid in an application I am writing to allow users to view and sometimes edit instances of my objects. Sometimes the user may have a file open in read/write mode where they can make changes to the file through the property grid. In other cases they may have a file open in read only mode, and should not be able to make any changes to the objects through the PropetyGrid. My classes also have dynamic properties which are returned by implementing ICustomTypeDescriptor. Which is why I really want to take advantage of the built in flexibility of a PropertyGrid control.
There doesn't seem to be an easy way to set a Property-grid to a read only mode. If I disable a PropertyGrid this also prevents the user from scrolling the list. So I'm thinking the best way to do this is to add ReadOnlyAttributes to the properties at run-time. Is there some other way?
I have found a very quick solution for thoses who do not care about the propertygrid being grayed out.
TypeDescriptor.AddAttributes(myObject, new Attribute[]{new ReadOnlyAttribute(true)});
propertyGrid1.SelectedObject = myObject;
Since you are implementing ICustomTypeDescriptor there is no need to add any attributes; you can just override IsReadOnly on the PropertyDescriptor. I'm thinking it should be pretty simple to write an intermediary type that mimics (via ICustomTypeDescriptor and TypeConverter) a wrapped type but always returns readonly PropertyDesciptor instances? Let me know if you want an example (it isn't trivial though).
You might also want to check whether something like this offers it built it.
My advice would be to write a custom control that inherits from the propertygrid control, and in that custom control, have a boolean value of readonly, and then override some things and check, if(readonly) then cancel the action
I ran into this one. I wanted a control that was read only but not grayed out.
Inherit from the property grid control and create your own read only version by adding the following code to override key presses
#Region "Non-greyed read only support"
Private isReadOnly As Boolean
Public Property [ReadOnly]() As Boolean
Get
Return Me.isReadOnly
End Get
Set(ByVal value As Boolean)
Me.isReadOnly = value
End Set
End Property
Protected Overrides Function ProcessDialogKey(ByVal keyData As Keys) As Boolean
If Me.isReadOnly Then Return True
Return MyBase.ProcessDialogKey(keyData)
End Function
Public Function PreFilterMessage(ByRef m As Message) As Boolean
If m.Msg = &H204 Then 'WM_RBUTTONDOWN
If Me.isReadOnly Then Return True
End If
Return False
End Function
#End Region
I ended up inheriting from PropertyGrid and select the parent category whenever a property is selected.
Simple and no need to use TypeDescriptor.
public class ReadOnlyPropGrid : PropertyGrid
{
public ReadOnlyPropGrid()
{
this.ToolbarVisible = false; // categories need to be always visible
}
protected override void OnSelectedGridItemChanged(SelectedGridItemChangedEventArgs e)
{
if (e.NewSelection.GridItemType == GridItemType.Property)
{
if (e.NewSelection.Parent != null && e.NewSelection.Parent.GridItemType == GridItemType.Category)
{
this.SelectedGridItem = e.NewSelection.Parent;
return;
}
}
}
}