Custom CollectionEditor doesn't serialize to aspx code - c#

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.

Related

How do I handle variations in HTMLControl Inner Text in a Page Object Pattern?

Here is a little background on the specifications of my project:
We use Specflow and Microsoft CodedUI Framework for UI Automation
I have built a PageFactory that combines three Abstract Base Classes : BasePage, BaseMap, and BaseValidator that all Maps, Pages, and Validators inherit
Our Application that we are automating has numerous workflows that make defined HTML Controls have different InnerText Values (HTMLComboBoxes for example)
Everything is and needs to be abstracted from the actual Specflow Test Code in the Page Object Pattern, no unique code can exist within a Specflow Step
In my Maps I have certain controls like a combobox that has an InnerText change if a certain workflow is selected. I need to build assertion and verification statements to make sure the InnerText is correct for the workflow that is selected. This is not a problem. However, I do not want to just define a new variable for every InnerText change(There are A LOT).
Is there any way I can account for the InnerText variations in the Page Object Pattern and not have to code a new variable for every single one?
Here is an example of a Map Entry:
public HtmlComboBox NextActionControlDropDownList()
{
var NextActionControlDropDownList = new PropertyExpressionCollection {
new PropertyExpression(HtmlComboBox.PropertyNames.Id, "MEDCHARTContent_EmmpsContent_nextActionControl_ActionDropDownList", PropertyExpressionOperator.EqualTo)
};
return Window.Find<HtmlComboBox>(NextActionControlDropDownList);
}
This is the Base Control definition. It can also be this:
public HtmlComboBox NextActionControlARFormalComplReview()
{
var NextActionControlARFormalComplReview = new PropertyExpressionCollection {
new PropertyExpression(HtmlComboBox.PropertyNames.Id, "MEDCHARTContent_EmmpsContent_nextActionControl_ActionDropDownList", PropertyExpressionOperator.EqualTo),
new PropertyExpression(HtmlComboBox.PropertyNames.InnerText, "--Select Action-- Return to USARC ", PropertyExpressionOperator.EqualTo)
};
return Window.Find<HtmlComboBox>(NextActionControlARFormalComplReview);
}
My thoughts so far were to maybe make another map and inherit it? But that wouldn't solve my initial problem of too many variables for a single control. I don't see how If statements would help either because it needs to be defined for the framework to find the control. Maybe I could store the differing values in a collection of sorts and have a parameter key value to access them... but that seems like I would run into a lot of issues.
If you try and see the methods under PropertyExpressionOperator you would see something called Contains.
new PropertyExpression(HtmlComboBox.PropertyNames.InnerText, "--Select Action--", PropertyExpressionOperator.Contains)

The specified value cannot be assigned to the collection

Edit
This bugs me for an almost year. I'll update the answer and add bounty.
I've custom control, which has dependency property
public class Graph : Control
{
public List<Figure> Figures
{
get { return (List<Figure>)GetValue(FiguresProperty); }
set { SetValue(FiguresProperty, value); }
}
public static readonly DependencyProperty FiguresProperty =
DependencyProperty.Register("Figures", typeof(List<Figure>), typeof(Graph),
new PropertyMetadata((d, e) => ((Graph)d).InvalidateVisual()));
...
}
Figure is the base class for all figures:
public abstract class Figure { ... }
public class LineFigure : Figure { ... }
public class XGridFigure : Figure { ... }
public class YGridFigure : Figure { ... }
...
Now look at screenshots below to see the problem: sometimes (after doing a change to xaml in other place) designer goes crazy about it and stop rendering the whole window, throwing exceptions, while code compiles and runs without problem. I can close this xaml (designer) and open it again to make problem go away. But it always reappears.
Question: is there something wrong on my side? Missing attribute? Wrong usage? How can I fix that problem?
Old question
Ugly situation.
I have 2 UserControl. In both hand-made control Graph is used. Graph has property Figures to specify List<Figure>. There are dozens of figures which have Figure as base.
In one UserControl it works fine, in other throws exception
The specified value cannot be assigned to the collection. The following type was expected: "Figure".
And I fail to see a difference what could cause a problem.
Here is problematic one screenshot
And here is working one
Despite of errors project compiles and runs, but if I need to do modification to problematic UserControl, then it's not showing any content (says "Invalid Markup"). Graphs are nearly the same, all 8 errors are shown for to just one UserControl.
What should I do? How to troubleshoot such errors? I exclude (completely) any problem with Graph because it runs without a single problem AND it works without problem for another UserControl. Visual Studio designer problem? Using 2013 Express for Windows Desktop.
Indeed the visual designer does not recognize the inheritance from Figure. One solution is to use IList as the Interface type:
public IList Figures
{
get
{
return (IList)GetValue (FiguresProperty);
}
set
{
SetValue (FiguresProperty, value);
}
}
public static readonly DependencyProperty FiguresProperty =
DependencyProperty.Register ("Figures", typeof (IList), typeof (Graph), new PropertyMetadata (new List<object>()));
That might look like a bit strange (because you give up type safetyness). But have a closer look at the WPF classes. They all do it that way (most likely for good reasons). Or WPF even creates collection classes like PathFigureCollection that implement both IList and IList<PathFigure>.
close the project, restart VS and reopen it. does it still list the errors? visual studio often seems to report "phantom errors", but they usually go away if you close and restart etc.
If the custom control is in the same solution or project, Visual Studio builds it (when it considers it necessary) so it can use the control in the designer.
Sometimes this built/cached version gets out of sync with the code files which causes the Xaml parser/syntax checker to get confused and display those wavy red lines.
I have had success with closing and reopening all designers that use the control but that is pretty annoying to keep on doing. In my experience the most reliable solution is to move the control into a separate solution and project and set a 'proper' reference to the dll.
I had a whole load of these errors in one project.
Eventually I found that the project did not have a reference to System.Xaml.
Adding a reference to System.Xaml removed all of the warnings.
The strange thing is that it didn't cause a runtime problem.

Any way to use object code in a User Control as a template for a new Object in a Sharepoint 2010 web part?

I'm trying to do something my teacher says can't be done; I would like to prove him wrong.
In the CreateChildControls method of my SharePoint 2010 webpart, I am referencing a User Control file called "ChartUserControl.ascx" in my project that contains the ASP.NET code for a WebChartControl object configured just the way I want it. WebChartControl has an ID of "OrderQtyChart".
What I want to do is take the code from that UserControl and use it create a new WebChartControl, called "chart", with matching configuration. I'm trying to do this because there are callbacks etc. that need to be performed on the chart after it's created to actually populate it with chart-stuff.
So, my code:
WebChartControl chart;
protected override void CreateChildControls()
{
ChartUserControl userControl = new ChartUserControl();
// referencing file ChartUserControl.ascx as an object
chart = userControl.FindControl("OrderQtyChart") as WebChartControl;
// or
chart = (WebChartControl)userControl.FindControl("OrderQtyChart");
// Trying to tell the code to create 'chart' using the code defined in object
"OrderQtyChart" located in ChartUserControl.ascx
}
Or something like that. In either instance above, 'chart' will return null.
I'm trying to use the front end code of OrderQtyChart as a template for 'chart'; they're both the same type of object and I don't get any errors until I try to create 'chart' on my page, at which point I'm told it's null.
Is there a way to do this? It would save me a ton of time not to have to configure 'chart' completely at creation time. Even if I have to reference my front-end code for OrderQtyChart a different way.
Thanks.
[Edited 7/9 for clarity]
What you are trying to do seems very well possible and I assume your teacher did not understand your question correctly. Here are a few tips on how this is done:
Object A could be one of these:
A visual control such as label or textbox. In this case your will have to traverse the visual controls from parent to child by doing direct parent.FindControl("ObjectA");
It is an instance of a class. This might be a MyClass or a new textbox that is created by code. In this case you'll have to create a public property that has a getter which returns ObjectA. although you can use FindControl in case ObjectA is a UI component that is created and added dynamically at run-time. Otherwise, you'll have to stick with property.
FindControl will not traverse the parent to child hierarchy, so you'll have to do a recursive method in order to successfully find the ObjectA or if you have access to its direct parent, call FindControl on that. More info here: http://geekswithblogs.net/QuandaryPhase/archive/2009/05/06/asp.net-recursive-findcontrol-amp-extension-methods.aspx
Page life cycle plays an important role here, so make sure that you keep it in mind or you'll end up with a null reference that is not really caused by FindControl
Gah, never mind. I realized I can just call the user control directly and I'm seriously overcomplicating this.
That's a whole new question, so I'll just start a different thread.

Access control's properties from another class in C# WPF

I'm in a mess with visibility between classes. Please, help me with this newbie question.
I have two controls (DatePickers from default WPF toolbox) which are in different windows, so in different classes. I can easily access these controls properties like datePicker1.Text from within its native class, i.e. in its native window, but when I try to reach datePicker1.Text from another window I get nothing.
I try to assign value of one datePicker to another, using reference to the window in my code:
string testStr;
...
AnotherWindow aw = new AnotherWindow();
testStr = aw.datePicker2.Text;
datePicker1.Text = testStr;
and it doesn't work
also I tried to do it through public property of a class, like:
public partial class AnotherWindow : Window
{
....
public string dateNearest
{
get { return datePicker2.Text; }
set { datePicker2.Text = value; }
}
....
and then use it in another window:
string testStr;
...
AnotherWindow aw = new AnotherWindow();
testStr = aw.dateNearest;
but also no value assigned.
Please, help me to understand this basic issue. I know there are other ways of accessing values in WPF like databinding, but I would like to understand basics first.
Unfortunately, the basics of WPF are data bindings. Doing it any other way is 'going against the grain', is bad practice, and is generally orders of magnitude more complex to code and to understand.
To your issue at hand, if you have data to share between views (and even if it's only one view), create a view model class which contains properties to represent the data, and bind to the properties from your view(s).
In your code, only manage your view model class, and don't touch the actual view with its visual controls and visual composition.
I'm using VS 2010 beta 2 right now which crashes regularly doing the simplest WPF coding, like trying to duplicate your question's code :) : but consider :
Is it possible that using this syntax will "do the right thing" :
public string dateNearest
{
get { return this.datePicker2.Text; }
set { this.datePicker2.Text = value; }
}
Edit 1 : Okay, I got a WPF replication of your code that didn't crash : using the above syntax I can both get and set the property in the "other window."
Edit 2 : The code also works using your original code :) Which, seemed to me to be "proper" the first time I read it. Are you setting that property before you read it ? : to my knowledge a DateTimePicker's Text property will be an empty string by default when first created.
Edit 3 : in response to Rem's request :
the main window has a button, 'button1 : which tests setting and getting the Public Property DTContent defined in the instance of the second Window named : 'WindowAdded : here's the 'Click event handler for that button in the main window's code :
private void button1_Click(object sender, RoutedEventArgs e)
{
WindowAdded wa = new WindowAdded();
wa.DTContent = DateTime.Now.ToString();
Console.WriteLine("dt = " + wa.DTContent);
}
Edit 4 : a better "real world" example : most cases you are going to want to create that instance of another window, and hold on to it, for re-use: imho : not have it exist only within the scope of a button's Click event. So consider, please :
Somewhere in the scope of the main window's code define a "place-holder" for the window(s) you will add : private WindowAdded wa;
In the event you select as most appropriate for creating the instance of that window : create the instance, and assign to your "place-holder" variable : then re-use it as needed. In WinForms I most often create required secondary windows that I will need to re-use references to the instances of to access something on them in the main form's load or shown events.
Discussion : of course, if your intent is to create "temporary" windows, and you don't need to re-use that reference to the new window's instance again, then creating it in the scope of some function is fine.
And, if the only thing you ever need to access on your second Window is the DateTimePicker, then you use the same technique suggested above, but create and hold to a reference to the instance of the DateTimePicker only.
As the others already pointed out, this is probably not the way to go, but you can use:
<object x:FieldModifier="public".../>
To set the object public.
See msdn for more info.

C# INotifyPropertyChanged on properties of a dynamically created object?

(update) ICustomTypeDescriptor works for my Windows Forms app, but not for Silverlight; Not supported. I will keep investigating this idea though and see where i get to.
(/update)
I have, say a few switch panels (for those that like analogies).
Each of these switch panels has switches that have a Name(string) can be in state(bool) of On or Off.
The switchpanel and switches are objects that have INotify interface on them.
Using the switches Names, I create a list of all possible switch names over the collection and create a dynamic class that has all these Names as properties.
SwitchPanel1 (Switches( Switch1 ("Main",On) , Switch2("Slave",Off)))
SwitchPanel2 (Switches( Switch1 ("Bilge",On) , Switch2("Main",Off)))
Produces a collection of
(Main,Bilge,Slave)
And a dynamic class is produced that has the properties:
SwitchPanel : (SwitchPanel)
Main : (Switch)
Bilge : (Switch)
Slave: (Switch)
The idea is that if the switch panel has a switch with the Name of the property, it is placed on that property. So using a bit of linq
propeties["Main"].SetValue(newSwitchType,SwitchPanel.Switches.FirstOrDefault(sw => sw.Name == "Main"));
I want to cast this new dynamic class to INotfyPropertyChanged AND catch the actual changes on these new properties, so if a switch changes state the dynamic object will report it.
Why? It needs to be displayed in a list view and the list view I'm using has its binding by supplying the Property name, and not the binding path.
It also attempts to catch INotify events by casting the object against INotifyPropertyChanged. This means it will sort and/or group when things change.
If you know of a better way to do this let me know. Please.
You probably don't need a dynamic class. You can implement runtime binding properties via ICustomTypeDescriptor / GetProperties(), creating your own PropertyDescriptor implementation that returns the named switch. It isn't clear what knows first about the change, but you could either use INotifyPropertyChanged, or the older property-specific change event, again tied to each property (so each PropertyDescriptor attaches to, for example, the event in the named switch.
Not trivial, but not impossible either.

Categories

Resources