├── LICENSE
├── README.md
├── demo
├── App.xaml
├── App.xaml.cs
├── ClassDiagram1.cd
├── CustomEditor.cs
├── DemoClasses.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── WpfPropertyGrid.cs
├── WpfPropertyGrid_Demo.csproj
└── WpfPropertyGrid_Demo.sln
├── images
├── ClassDiagram.png
├── Person.png
├── PersonProps.png
├── Place.png
├── PlaceProps1.png
├── PlaceProps2.png
├── Vehicle.png
├── VehicleProps1.png
├── VehicleProps2.png
├── WorkflowFoundationDesigner.jpg
├── screenshot1.png
└── screenshot2.PNG
└── src
└── WpfPropertyGrid.cs
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011 - 2024 - Jaime Olivares
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wpf-propertygrid
2 | Repackaging of Workflow Foundation's property grid for general use in WPF applications
3 |
4 |  
5 |
6 | ## Disclaimer
7 | The following implementation was originally developed in 2010 and published in CodeProject. After so many years, it seems that it still attracts interest and a few stars, so I decided to move the entire Readme to GitHub.
8 |
9 | I no longer maintain the source code or advise on how to implement it. However, I encourage active users to help others in the Issues section. Pull Requests may be accepted if they have broad utility.
10 |
11 | ## Introduction
12 |
13 | While working with Windows Workflow Foundation 4.0, I realized that the `PropertyInspectorView` control is a fully-fledged WPF PropertyGrid control, including support for the customization of attributes and custom editors. A PropertyInspectorView is associated with a parent `WorkflowDesigner` object and a sibling workflow `View` object, which is the real drag-and-drop canvas, as shown in this MSDN's screenshot:
14 |
15 | 
16 | _Workflow Foundation example. Left: Activities Toolbox, Middle: Designer View, Right: Property Inspector._
17 |
18 | ## Internal Architecture
19 | The overall approach to making the `PropertyInspector` available for general-purpose use is the following:
20 |
21 | - Derive a new control from a `Grid` class. The grid will contain the real UI element.
22 | - Incorporate a Workflow Foundation's `WorkflowDesigner` object as a private class member.
23 | - For the designer object, add the corresponding `PropertyInspectorView` as a grid's child. Although it is exposed as a Grid, its real type is `PropertyInspector`.
24 | - Capture some methods of the `PropertyInspector` by using reflection, for further use.
25 | - Implement `SelectedObject` and `SelectedObjects` properties, as in regular PropertyGrid, and handle the change of selection at the `PropertyInspector`.
26 | - Add a `GridSplitter` and a `TextBlock` to mimic the HelpText functionality of the original PropertyGrid.
27 |
28 | The following diagram depicts the internal architecture of the WpfPropertyGrid class, as explained in previous lines:
29 |
30 | 
31 |
32 | It is needed at least to invoke the public constructor and set the `SelectedObject` or `SelectedObjects`. The `RefreshPropertyList` method will be helpful in refreshing the control when the selected object has been changed externally.
33 |
34 | Set the `HelpVisible` property to show the property descriptions at the bottom, while the `ToolbarVisible` will show or hide the upper toolbar. Those properties have the same name as Winform´s PropertyGrid, in order to keep some degree of compatibility.
35 |
36 | The same is true for the `PropertySort` property, which accepts a PropertySort enumeration type. It allows to group the properties by categories or show them in a flat fashion.
37 |
38 | The `FontAndColorData` property can be used to restyle the control, since it internally sets the `WorkflowDesigner.PropertyInspectorFontAndColorData` property, but little information is available on the Internet.
39 |
40 | ## Basic Usage - Person Class
41 | The supplied demo project will allow you to test all the `WpfropertyGrid` capabilities. Three classes are defined in DemoClasses.cs, with different features, like custom attributes and editors. Here is the declaration of the first and simplest one:
42 |
43 | ````csharp
44 | public class Person
45 | {
46 | public enum Gender { Male, Female }
47 |
48 | #region private fields
49 | private string[] _Names = new string[3];
50 | #endregion
51 |
52 | // The following properties are wrapping an array of strings
53 | #region Public Properties
54 | [Category("Name")]
55 | [DisplayName("First Name")]
56 | public string FirstName
57 | {
58 | set { _Names[0] = value; }
59 | get { return _Names[0]; }
60 | }
61 |
62 | [Category("Name")]
63 | [DisplayName("Mid Name")]
64 | public string MidName
65 | {
66 | set { _Names[1] = value; }
67 | get { return _Names[1]; }
68 | }
69 |
70 | [Category("Name")]
71 | [DisplayName("Last Name")]
72 | public string LastName
73 | {
74 | set { _Names[2] = value; }
75 | get { return _Names[2]; }
76 | }
77 |
78 | // The following are auto-implemented properties (C# 3.0 and up)
79 | [Category("Characteristics")]
80 | [DisplayName("Gender")]
81 | public Gender PersonGender { get; set; }
82 |
83 | [Category("Characteristics")]
84 | [DisplayName("Birth Date")]
85 | public DateTime BirthDate { get; set; }
86 |
87 | [Category("Characteristics")]
88 | public int Income { get; set; }
89 |
90 | // Other cases of hidden read-only property and formatted property
91 | [DisplayName("GUID"), ReadOnly(true), Browsable(true)]
92 | public string GuidStr
93 | {
94 | get { return Guid.ToString(); }
95 | }
96 |
97 | [Browsable(false)] // this property will not be displayed
98 | public System.Guid Guid
99 | {
100 | get;
101 | private set;
102 | }
103 | #endregion
104 |
105 | public Person()
106 | {
107 | // default values
108 | for (int i = 0; i < 3; i++)
109 | _Names[i] = "";
110 | this.PersonGender = Gender.Male;
111 | this.Guid = System.Guid.NewGuid();
112 | }
113 |
114 | public override string ToString()
115 | {
116 | return string.Format("{0} {1} {2}", FirstName,
117 | MidName, LastName).Trim().Replace(" ", " ");
118 | }
119 | }
120 | ````
121 |
122 | Here is the class diagram and how it appears in the demo application:
123 |
124 |  
125 |
126 | Notice that the control will show just the properties, not the fields. As we are using C# 3.0 or 4.0, we can avoid declaring the underlying fields by using auto-implemented properties, whenever is convenient.
127 |
128 | To show the properties of a `Person` object is quite simple; just assign it to the control's `SelectedObject` property, as shown:
129 |
130 | ````csharp
131 | PropertyGrid1.SelectedObject = thePerson; // typeof(thePerson) == Person
132 | ````
133 |
134 | ## Basic Attributes
135 | In the `Person` class implementation, you will notice some properties that have attributes (those with square brackets); they won't affect your class behavior, but will do with the property grid. These attributes are similar to those implemented in Winforms's `PropertyGrid`. Let's see them in detail.
136 |
137 | - `Category`: Lets you specify a category group for the affected property. A category appears by default at the property grid with a gray background, as you can see in the first screenshot. If the property doesn't have a Category attribute, it will belong to a blank category group, as with the GUID property in the previous screenshot. It is recommended to always specify a category for each property.
138 | - `DisplayName`: Will be useful when you want to display a property name that is different from the real one. It is usually used to increase readability with white spaces, or to abbreviate the name.
139 | - `ReadOnly`: When set to `true`, will prevent the property from being edited; it will be just shown in the property grid. To prevent the read-only properties from being hidden, it will necessary to mark them as `Browsable=true`, as with the `GUIDStr` property.
140 | - `Browsable`: When set to `false`, the property will not be shown. It is useful when you have a property you don't want to show, like the GUID property in the first example.
141 |
142 | All these attributes are declared in the `System.ComponentModel` namespace and automatically recognized by the property inspector.
143 |
144 | ## Custom Properties - Vehicle Class
145 |
146 | While the simplest implementation of `WpfPropertyGrid` exposes all the properties of a class (except of those with the `Browsable` attribute set to false), the `ICustomProperties` interface will allow some properties to be conditionally exposed. There are some customizations needed to accomplish this, as in the following example:
147 |
148 | ````csharp
149 | public class Vehicle :
150 | ICustomTypeDescriptor, INotifyPropertyChanged
151 | {
152 | public enum CarType { Sedan, StationWagon, Coupe,
153 | Roadster, Van, Pickup, Truck }
154 | public enum CarBrand { Acura, Audi, BMW, Citroen,
155 | Ford, GMC, Honda, Lexus, Mercedes, Mitsubishi,
156 | Nissan, Porshe, Suzuki, Toyota, VW, Volvo }
157 |
158 | #region Private fields
159 | private CarType _TypeOfCar;
160 | #endregion
161 |
162 | #region Public Properties
163 | [Category("Classification")]
164 | public CarBrand Brand { get; set; }
165 |
166 | [Category("Classification")]
167 | [DisplayName("Type")]
168 | public CarType TypeOfCar
169 | {
170 | get { return this._TypeOfCar; }
171 | set {
172 | this._TypeOfCar = value;
173 | NotifyPropertyChanged("TypeOfCar");
174 | }
175 | }
176 |
177 | [Category("Classification")]
178 | public string Model { get; set; }
179 |
180 | [Category("Identification")]
181 | [DisplayName("Manuf.Year")]
182 | public int Year { get; set; }
183 |
184 | [Category("Identification")]
185 | [DisplayName("License Plate")]
186 | public string Plate { get; set; }
187 |
188 | // Will shown only for Pickup and Truck
189 | [Category("Capacity")]
190 | [DisplayName("Volume (ft³)")]
191 | public int Volume { get; set; }
192 |
193 | [Category("Capacity")]
194 | [DisplayName("Payload (kg)")]
195 | public int Payload { get; set; }
196 |
197 | [Category("Capacity")]
198 | [DisplayName("Crew cab?")]
199 | public bool CrewCab { get; set; }
200 | #endregion
201 |
202 | #region ICustomTypeDescriptor Members
203 | public AttributeCollection GetAttributes() ...
204 | public string GetClassName() ...
205 | public string GetComponentName() ...
206 | public TypeConverter GetConverter() ...
207 | public EventDescriptor GetDefaultEvent() ...
208 | public PropertyDescriptor GetDefaultProperty() ...
209 | public object GetEditor(Type editorBaseType)
210 | public EventDescriptorCollection
211 | GetEvents(Attribute[] attributes) ...
212 | public EventDescriptorCollection GetEvents() ...
213 | public object
214 | GetPropertyOwner(PropertyDescriptor pd) ...
215 | public PropertyDescriptorCollection
216 | GetProperties(Attribute[] attributes) ...
217 |
218 | // Method implemented to expose Capacity properties
219 | // conditionally, depending on TypeOfCar
220 | public PropertyDescriptorCollection GetProperties()
221 | {
222 | var props = new PropertyDescriptorCollection(null);
223 |
224 | foreach (PropertyDescriptor prop in
225 | TypeDescriptor.GetProperties(this, true))
226 | {
227 | if (prop.Category=="Capacity" &&
228 | (this.TypeOfCar != CarType.Pickup &&
229 | this.TypeOfCar != CarType.Truck))
230 | continue;
231 | props.Add(prop);
232 | }
233 |
234 | return props;
235 | }
236 | #endregion
237 |
238 | #region INotifyPropertyChanged Members
239 | public event PropertyChangedEventHandler PropertyChanged;
240 |
241 | private void NotifyPropertyChanged(String info)
242 | {
243 | if (PropertyChanged != null)
244 | PropertyChanged(this, new PropertyChangedEventArgs(info));
245 | }
246 | #endregion
247 | }
248 | ````
249 |
250 |   
251 |
252 | Notice that the most important method needed to implement the `PropertyGrid.ICustomProperties` interface is `GetProperties()`. This method shall return all the property names you want to expose as an array, depending on some conditions. In this example, if the car type is a _PickUp_ or _Truck_, the `Volume`, `Payload`, and `CrewCab` properties will be exposed.
253 |
254 | # Custom Editors - Place Class
255 |
256 | > Disclaimer: Although the intent of this article is not to go deep with Editor customization, I will show a couple of examples of the two kinds of editors: extended and dialog-based. The [Workflow Foundation documentation](https://learn.microsoft.com/en-us/dotnet/framework/windows-workflow-foundation/customizing-the-workflow-design-experience) provides more information.
257 |
258 | Custom editors are the most powerful feature of this control. There are several tricks you can do with it. By default, the control will provide an editor for all the fundamental classes: `int`, `float`, `double`, etc., and also for strings and enumerations, the latter as a `ComboBox`. If you have a custom class object as a property, it will show the string representation but just as read-only because the grid control doesn't know how to edit it.
259 |
260 | The `Place` class implementation shows both. Despite the kind of editor, it must be derived from the `PropertyValueEditor` class, as we will see in detail later.
261 |
262 | To specify a custom editor for a property, an `EditorAttribute` attribute must be added to the property declaration, as with `CountryInfo` and `Picture` properties.
263 |
264 | ````csharp
265 | public class Place
266 | {
267 | public struct CountryInfo
268 | {
269 | public static readonly CountryInfo[] Countries = {
270 | // African countries
271 | new CountryInfo(Continent.Africa , "AO", "ANGOLA" ),
272 | new CountryInfo(Continent.Africa, "CM", "CAMEROON" ),
273 | // American countries
274 | new CountryInfo(Continent.America, "MX", "MEXICO" ),
275 | new CountryInfo(Continent.America, "PE", "PERU" ),
276 | // Asian countries
277 | new CountryInfo(Continent.Asia, "JP", "JAPAN" ),
278 | new CountryInfo(Continent.Asia, "MN", "MONGOLIA" ),
279 | // European countries
280 | new CountryInfo(Continent.Europe, "DE", "GERMANY" ),
281 | new CountryInfo(Continent.Europe, "NL", "NETHERLANDS" ),
282 | // Oceanian countries
283 | new CountryInfo(Continent.Oceania, "AU", "AUSTRALIA" ),
284 | new CountryInfo(Continent.Oceania, "NZ", "NEW ZEALAND" )
285 | };
286 |
287 | public Continent Contin { get; set; }
288 | public string Abrev { get; set; }
289 | public string Name { get; set; }
290 |
291 | public override string ToString()
292 | {
293 | return string.Format("{0} ({1})", Name, Abrev);
294 | }
295 | public CountryInfo(Continent _continent,
296 | string _abrev, string _name) : this()
297 | {
298 | this.Contin = _continent;
299 | this.Abrev = _abrev;
300 | this.Name = _name;
301 | }
302 | }
303 |
304 | #region Private fields
305 | private string[] _Address = new string[4];
306 | #endregion
307 |
308 | #region Public properties
309 | [Category("Address")]
310 | public string Street
311 | {
312 | get { return _Address[0]; }
313 | set { _Address[0] = value; }
314 | }
315 |
316 | [Category("Address")]
317 | public string City
318 | {
319 | get { return _Address[1]; }
320 | set { _Address[1] = value; }
321 | }
322 |
323 | [Category("Address")]
324 | public string Province
325 | {
326 | get { return _Address[2]; }
327 | set { _Address[2] = value; }
328 | }
329 |
330 | [Category("Address")]
331 | public string Postal
332 | {
333 | get { return _Address[3]; }
334 | set { _Address[3] = value; }
335 | }
336 |
337 | // Custom editor for the following 2 properties
338 | [Category("Address")]
339 | [Editor(typeof(CountryEditor), typeof(PropertyValueEditor))]
340 | public CountryInfo Country { get; set; }
341 |
342 | [Category("Characteristics")]
343 | [Editor(typeof(PictureEditor), typeof(PropertyValueEditor))]
344 | public BitmapImage Picture { get; set; }
345 |
346 | [Category("Characteristics")]
347 | public int Floors { get; set; }
348 |
349 | [Category("Characteristics")]
350 | public int CurrentValue { get; set; }
351 | #endregion
352 |
353 | public Place()
354 | {
355 | for (int i = 0; i < _Address.Length; i++)
356 | _Address[i] = string.Empty;
357 | this.Country = CountryInfo.Countries[0];
358 | }
359 | }
360 | ````
361 |
362 |   
363 |
364 | As mentioned, there are two examples of custom editor implementations in the `Place` class; the first one, `CountryEditor`, is an extended editor. It asks for a country with two `ComboBox`es: one for `Continent` and one for `Country`, as shown in the screenshot. To simplify the demonstration, the required XAML `DataTemplate` is placed in the source code file, which is not so usual:
365 |
366 | ````csharp
367 | class CountryEditor : ExtendedPropertyValueEditor
368 | {
369 | public CountryEditor()
370 | {
371 | // Template for normal view
372 | string template1 = @"...xaml template here...";
373 |
374 | // Template for extended view. Shown when dropdown button is pressed.
375 | string template2 = @"...xaml template here...";
376 |
377 | // Load templates
378 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template1)))
379 | {
380 | this.InlineEditorTemplate = XamlReader.Load(sr) as DataTemplate;
381 | }
382 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template2)))
383 | {
384 | this.ExtendedEditorTemplate = XamlReader.Load(sr) as DataTemplate;
385 | }
386 | }
387 | }
388 | ````
389 |
390 | For an extended editor, it has to be derived from `ExtendedPropertyValueEditor` class. That will allow the property grid to drop down a customized control to input the property data.
391 |
392 | The constructor should load both templates, the normal one and the extended one, from some XAML `DataTemplate` declarations. Usually those templates are placed in a XAML Resource file.
393 |
394 | The second example of a custom editor is `PictureEditor`; it is different from an extended editor because it shows a new dialog when the dropdown button is pressed, so it will require implementing that window separately. Also, it is derived from a different base class: `DialogPropertyValueEditor`. The sample class is shown partially for abbreviation purposes:
395 |
396 | ````csharp
397 | class PictureEditor : DialogPropertyValueEditor
398 | {
399 | // Window to show the current image and optionally pick a different one
400 | public class ImagePickerWindow : Window
401 | {
402 | // regular window implementation here
403 | }
404 |
405 | public PictureEditor()
406 | {
407 | string template = @"...xmal template here...";
408 |
409 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template)))
410 | {
411 | this.InlineEditorTemplate = XamlReader.Load(sr) as DataTemplate;
412 | }
413 | }
414 |
415 | // Open the dialog to pick image, when the dropdown button is pressed
416 | public override void ShowDialog(PropertyValue propertyValue, IInputElement commandSource)
417 | {
418 | ImagePickerWindow window = new ImagePickerWindow(propertyValue.Value as BitmapImage);
419 | if (window.ShowDialog().Equals(true))
420 | {
421 | var ownerActivityConverter = new ModelPropertyEntryToOwnerActivityConverter();
422 | ModelItem activityItem = ownerActivityConverter.Convert(propertyValue.ParentProperty,
423 | typeof(ModelItem), false, null) as ModelItem;
424 | using (ModelEditingScope editingScope = activityItem.BeginEdit())
425 | {
426 | propertyValue.Value = window.TheImage;
427 | editingScope.Complete(); // commit the changes
428 | }
429 | }
430 | }
431 | }
432 | ````
433 |
434 | ## Multiple selection
435 |
436 | While single selection can be done by setting the `SelectedObject` property to any value, multiple selection is achieved by setting the `SelectedObjects` property.
437 |
438 | When multiple objects are selected, the Type Label at the top of the control will show the word "<multiple>" to the right of the type. If all the selected objects are the same type, the type name is shown (see screenshot below). Otherwise, the type _"Object"_ is displayed.
439 |
440 | All the properties that have the same type and name for all the selected objects are shown, even if the selected objects are not of the same type. Try using Person and Place in the Demo application, which shares the FirstName and LastName properties.
441 |
442 | 
443 |
444 | ## Help text
445 | The textbox at the bottom of the control is called the HelpText. It will shown a property description set with the `DescriptionAttribute` attribute (see screenshot above).
446 |
447 | When multiple selected objects are present, the description will be shown only if all the selected objects are of the same type.
448 |
449 | The HelpText box can be shown or hidden by setting the `HelpVisible` property in the `PropertyGrid`.
450 |
451 | ## How to use it
452 |
453 | The WpfPropertyGrid can be embedded directly into your application. It doesn't need to be compiled in a separate DLL. To include it in some XAML declaration, you have to specify the correct namespace (a local `System.Windows.Control`) and add the corresponding tag to your WPF window or dialog:
454 |
455 | ````xml
456 |
461 |
462 |
463 |
464 |
465 |
466 |
467 | `````
468 |
469 | ### Dependency Properties
470 |
471 | As the control properties are _Dependency Properties_, they can be bound to other elements in the container dialog or window like in the demo application (simplified):
472 |
473 | ````xml
474 |
481 |
482 |
487 | ````
488 |
489 | The demo application has been built with Visual Studio 2010. As the WPF property inspector is a new feature in .net 4.0, this implementation won't be available for applications written for .net 3.0 or 3.5, even when they implement Workflow Foundation.
490 |
491 | To use this control, you just need to add the WpfPropertyGrid.cs file to your project. Some references will be required in your solution:
492 |
493 | - System.Activities
494 | - System.Activities.Core.Presentation
495 | - System.Activities.Presentation
496 |
497 |
--------------------------------------------------------------------------------
/demo/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/demo/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Windows;
7 |
8 | namespace WpfPropertyGrid_Demo
9 | {
10 | ///
11 | /// Interaction logic for App.xaml
12 | ///
13 | public partial class App : Application
14 | {
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/ClassDiagram1.cd:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/CustomEditor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Activities.Presentation.Converters;
3 | using System.Activities.Presentation.Model;
4 | using System.Activities.Presentation.PropertyEditing;
5 | using System.IO;
6 | using System.Text;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Markup;
10 | using System.Windows.Media;
11 | using System.Windows.Media.Imaging;
12 | using Microsoft.Win32;
13 |
14 | // Sample XAML Templates have been incorporated here for simplification of source code
15 | // Usually they are placed into a Resource file
16 |
17 | namespace WpfPropertyGrid_Demo
18 | {
19 | ///
20 | /// Custom editor to select continent/country
21 | ///
22 | class CountryEditor : ExtendedPropertyValueEditor
23 | {
24 | public CountryEditor()
25 | {
26 | // Template for normal view
27 | string template1 = @"
28 |
32 |
33 |
35 |
36 |
37 | ";
38 |
39 | // Template for extended view. Shown when dropdown button is pressed.
40 | string template2 = @"
41 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | ";
69 |
70 | // Load templates
71 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template1)))
72 | {
73 | this.InlineEditorTemplate = XamlReader.Load(sr) as DataTemplate;
74 | }
75 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template2)))
76 | {
77 | this.ExtendedEditorTemplate = XamlReader.Load(sr) as DataTemplate;
78 | }
79 | }
80 | }
81 |
82 | ///
83 | /// Custom editor to select a picture
84 | ///
85 | class PictureEditor : DialogPropertyValueEditor
86 | {
87 | // Window to show the current image and optionally pick a different one
88 | public class ImagePickerWindow : Window
89 | {
90 | public BitmapImage TheImage = null;
91 | public Image ImageControl = null;
92 |
93 | public ImagePickerWindow(BitmapImage bitmap)
94 | {
95 | this.TheImage = bitmap;
96 |
97 | this.Title = "Select Picture:";
98 | this.Width = 195;
99 | this.Height = 235;
100 | this.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
101 | this.WindowStyle = System.Windows.WindowStyle.ToolWindow;
102 | this.ResizeMode = System.Windows.ResizeMode.NoResize;
103 | this.ShowInTaskbar = false;
104 |
105 | var image = new Image();
106 | image.Width = 180;
107 | image.Height = 180;
108 | image.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
109 | image.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
110 |
111 | var border = new Border();
112 | border.Width = 180;
113 | border.Height = 180;
114 | border.BorderThickness = new Thickness(1);
115 | border.BorderBrush = Brushes.Black;
116 | border.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
117 | border.VerticalAlignment = System.Windows.VerticalAlignment.Top;
118 | border.Margin = new Thickness(5, 5, 0, 0);
119 | border.Child = image;
120 |
121 | var button1 = new Button();
122 | button1.Content = "Pick...";
123 | button1.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
124 | button1.VerticalAlignment = System.Windows.VerticalAlignment.Top;
125 | button1.Width = 70;
126 | button1.Height = 20;
127 | button1.Margin = new Thickness(5, 190, 0, 0);
128 | button1.Click += new RoutedEventHandler(PickButton_Click);
129 |
130 | var button2 = new Button();
131 | button2.Content = "OK";
132 | button2.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
133 | button2.VerticalAlignment = System.Windows.VerticalAlignment.Top;
134 | button2.Width = 70;
135 | button2.Height = 20;
136 | button2.Margin = new Thickness(85, 190, 0, 0);
137 | button2.Click += new RoutedEventHandler(OKButton_Click);
138 |
139 | var grid = new Grid();
140 | grid.Children.Add(border);
141 | grid.Children.Add(button1);
142 | grid.Children.Add(button2);
143 | this.AddChild(grid);
144 |
145 | this.ImageControl = image;
146 | if (bitmap != null)
147 | ImageControl.Source = bitmap;
148 | }
149 |
150 | void OKButton_Click(object sender, RoutedEventArgs e)
151 | {
152 | this.DialogResult = true;
153 | this.Close();
154 | }
155 | void PickButton_Click(object sender, RoutedEventArgs e)
156 | {
157 | OpenFileDialog dialog = new OpenFileDialog();
158 |
159 | dialog.CheckFileExists = true;
160 | dialog.DefaultExt = ".jpg";
161 | dialog.Filter = "Picture Files (*.jpg, *.bmp, *.png)|*.jpg;*.bmp;*.png";
162 | dialog.Multiselect = false;
163 | dialog.Title = "Select Picture";
164 |
165 | if (dialog.ShowDialog().Equals(true))
166 | {
167 | try
168 | {
169 | BitmapImage image = new BitmapImage();
170 | image.BeginInit();
171 | image.UriSource = new Uri(dialog.FileName, UriKind.RelativeOrAbsolute);
172 | image.EndInit();
173 |
174 | this.TheImage = image;
175 | this.ImageControl.Source = image;
176 | }
177 | catch { }
178 | }
179 | }
180 | }
181 |
182 | public PictureEditor()
183 | {
184 | string template = @"
185 |
189 |
190 | ...
192 |
193 |
194 | ";
195 |
196 | using (var sr = new MemoryStream(Encoding.UTF8.GetBytes(template)))
197 | {
198 | this.InlineEditorTemplate = XamlReader.Load(sr) as DataTemplate;
199 | }
200 | }
201 |
202 | // Open the dialog to pick image, when the dropdown button is pressed
203 | public override void ShowDialog(PropertyValue propertyValue, IInputElement commandSource)
204 | {
205 | ImagePickerWindow window = new ImagePickerWindow(propertyValue.Value as BitmapImage);
206 | if (window.ShowDialog().Equals(true))
207 | {
208 | var ownerActivityConverter = new ModelPropertyEntryToOwnerActivityConverter();
209 | ModelItem activityItem = ownerActivityConverter.Convert(propertyValue.ParentProperty, typeof(ModelItem), false, null) as ModelItem;
210 | using (ModelEditingScope editingScope = activityItem.BeginEdit())
211 | {
212 | propertyValue.Value = window.TheImage;
213 | editingScope.Complete(); // commit the changes
214 |
215 | var control = commandSource as Control;
216 | var oldData = control.DataContext;
217 | control.DataContext = null;
218 | control.DataContext = oldData;
219 | }
220 | }
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/demo/DemoClasses.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Activities.Presentation.PropertyEditing;
3 | using System.Collections.ObjectModel;
4 | using System.ComponentModel;
5 | using System.Windows.Data;
6 | using System.Windows.Media.Imaging;
7 |
8 | // Sample classes to be shown in WpfPropertyGrid
9 | // They have regular and customized properties, and custom editors as well
10 |
11 | namespace WpfPropertyGrid_Demo
12 | {
13 | // Simple sample class with properties of several types
14 | public class Person
15 | {
16 | public enum Gender { Male, Female }
17 |
18 | #region private fields
19 | private string[] _Names = new string[3];
20 | #endregion
21 |
22 | // The following properties are wrapping an array of strings
23 | #region Public Properties
24 | [Category("Name")]
25 | [DisplayName("First Name")]
26 | public string FirstName
27 | {
28 | set { _Names[0] = value; }
29 | get { return _Names[0]; }
30 | }
31 |
32 | [Category("Name")]
33 | [DisplayName("Mid Name")]
34 | public string MidName
35 | {
36 | set { _Names[1] = value; }
37 | get { return _Names[1]; }
38 | }
39 |
40 | [Category("Name")]
41 | [DisplayName("Last Name")]
42 | public string LastName
43 | {
44 | set { _Names[2] = value; }
45 | get { return _Names[2]; }
46 | }
47 |
48 | // The following are autoimplemented properties (C# 3.0 and up)
49 | [Category("Characteristics")]
50 | [DisplayName("Gender")]
51 | public Gender PersonGender { get; set; }
52 |
53 | [Category("Characteristics")]
54 | [DisplayName("Birth Date")]
55 | public DateTime BirthDate { get; set; }
56 |
57 | [Category("Characteristics")]
58 | public int Income { get; set; }
59 |
60 | // Other cases of hidden read-only property and formatted property
61 | [DisplayName("GUID"), ReadOnly(true), Browsable(true)] // many attributes defined in the same row
62 | public string GuidStr
63 | {
64 | get { return Guid.ToString(); }
65 | }
66 |
67 | [Browsable(false)] // this property will not be displayed
68 | public System.Guid Guid
69 | {
70 | get;
71 | private set;
72 | }
73 | #endregion
74 |
75 | public Person()
76 | {
77 | // default values
78 | for (int i = 0; i < 3; i++)
79 | _Names[i] = "";
80 | this.PersonGender = Gender.Male;
81 | this.Guid = System.Guid.NewGuid();
82 | }
83 |
84 | public override string ToString()
85 | {
86 | return string.Format("{0} {1} {2}", FirstName, MidName, LastName).Trim().Replace(" ", " ");
87 | }
88 | }
89 |
90 | // Sample class with custom properties. Implements ICustomTypeDescriptor interface.
91 | public class Vehicle : ICustomTypeDescriptor, INotifyPropertyChanged
92 | {
93 | public enum CarType { Sedan, StationWagon, Coupe, Roadster, Van, Pickup, Truck }
94 | public enum CarBrand { Acura, Audi, BMW, Citroen, Ford, GMC, Honda, Lexus, Mercedes, Mitsubishi, Nissan, Porshe, Suzuki, Toyota, VW, Volvo }
95 |
96 | #region Private fields
97 | private CarType _TypeOfCar;
98 | #endregion
99 |
100 | #region Public Properties
101 | [Category("Classification")]
102 | public CarBrand Brand { get; set; }
103 |
104 | [Category("Classification")]
105 | [DisplayName("Type")]
106 | [Description("Extra fields can be shown depending on type of car.")]
107 | public CarType TypeOfCar
108 | {
109 | get
110 | {
111 | return this._TypeOfCar;
112 | }
113 | set
114 | {
115 | this._TypeOfCar = value;
116 | NotifyPropertyChanged("TypeOfCar");
117 | }
118 | }
119 |
120 | [Category("Classification")]
121 | public string Model { get; set; }
122 |
123 | [Category("Identification")]
124 | [DisplayName("Manuf.Year")]
125 | public int Year { get; set; }
126 |
127 | [Category("Identification")]
128 | [DisplayName("License Plate")]
129 | public string Plate { get; set; }
130 |
131 | // Will shown only for Pickup and Truck
132 | [Category("Capacity")]
133 | [DisplayName("Volume (ft³)")]
134 | public int Volume { get; set; }
135 |
136 | [Category("Capacity")]
137 | [DisplayName("Payload (kg)")]
138 | public int Payload { get; set; }
139 |
140 | [Category("Capacity")]
141 | [DisplayName("Crew cab?")]
142 | public bool CrewCab { get; set; }
143 | #endregion
144 |
145 | #region ICustomTypeDescriptor Members
146 | public AttributeCollection GetAttributes()
147 | {
148 | return TypeDescriptor.GetAttributes(this, true);
149 | }
150 | public string GetClassName()
151 | {
152 | return TypeDescriptor.GetClassName(this, true);
153 | }
154 | public string GetComponentName()
155 | {
156 | return null;
157 | }
158 | public TypeConverter GetConverter()
159 | {
160 | return TypeDescriptor.GetConverter(this, true);
161 | }
162 | public EventDescriptor GetDefaultEvent()
163 | {
164 | return TypeDescriptor.GetDefaultEvent(this, true);
165 | }
166 | public PropertyDescriptor GetDefaultProperty()
167 | {
168 | return TypeDescriptor.GetDefaultProperty(this, true);
169 | }
170 | public object GetEditor(Type editorBaseType)
171 | {
172 | return null;
173 | }
174 | public EventDescriptorCollection GetEvents(Attribute[] attributes)
175 | {
176 | return TypeDescriptor.GetEvents(this, attributes, true);
177 | }
178 | public EventDescriptorCollection GetEvents()
179 | {
180 | return TypeDescriptor.GetEvents(this, true);
181 | }
182 | public object GetPropertyOwner(PropertyDescriptor pd)
183 | {
184 | return this;
185 | }
186 | public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
187 | {
188 | return this.GetProperties();
189 | }
190 |
191 | // Method implemented to expose Volume and PayLoad properties conditionally, depending on TypeOfCar
192 | public PropertyDescriptorCollection GetProperties()
193 | {
194 | var props = new PropertyDescriptorCollection(null);
195 |
196 | foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this, true))
197 | {
198 | if (prop.Category=="Capacity" && (this.TypeOfCar != CarType.Pickup && this.TypeOfCar != CarType.Truck))
199 | continue;
200 | props.Add(prop);
201 | }
202 |
203 | return props;
204 | }
205 | #endregion
206 |
207 | #region INotifyPropertyChanged Members
208 | public event PropertyChangedEventHandler PropertyChanged;
209 |
210 | private void NotifyPropertyChanged(String info)
211 | {
212 | if (PropertyChanged != null)
213 | {
214 | PropertyChanged(this, new PropertyChangedEventArgs(info));
215 | }
216 | }
217 | #endregion
218 | }
219 |
220 | public enum Continent { Africa = 1, America = 2, Asia = 3, Europe = 4, Oceania = 5 }
221 |
222 | public class FilteredCountriesConverter : IMultiValueConverter
223 | {
224 | public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
225 | {
226 | ObservableCollection items = new ObservableCollection(Place.CountryInfo.Countries);
227 | CollectionView cv = CollectionViewSource.GetDefaultView(items) as CollectionView;
228 |
229 | if (values != null && values[0] != null)
230 | {
231 | Continent continent = (Continent)values[0];
232 | if (continent > 0)
233 | cv.Filter = new Predicate