When you bind a control's property using DataBindings to a datasource, the property grid will show a little purple or black symbol in that property coresponding grid item.
Even when you place your PropertyGrid on the form and set its SelectedObject property to the control with the bound property, that PropertyGrid on the form will show that symbol as well.
But only at design time.
Is there a (simple) way to make the very same PropertyGrid to show this symbol at runtime?
It is handled by Visual Studio designer internal things. But you also can add this feature to PropertyGrid:
You need to implement IPropertyValueUIService and using reflection, assign an instance of the service to the grid entries which should show the glyph. This implementation has a method GetPropertyUIValueItems which can be used to provide that glyph and a tooltip to show near the property label in PropertyGrid. Those values will be used in PaintLabel method of the property grid entry.
Then create an inherited PropertyGrid and override OnSelectedObjectsChanged and OnPropertySortChanged. In those method for each property grid entry item which is presenting a property which is in data bindings collection, set an instance of the implemented IPropertyValueUIService as value of pvSvc private property of the PropertyGrid and attach an event handler which will be called when PropertyGrid requests for additional information about the property. By attaching an event handler using GetPropertyUIValueItems to you can return the tooltip and image which you are going to show in front of the property.
Example
You can download the full example here:
r-aghaei/PropertyGridDataBindingGlyph
You can find main classes of the implementation as follows.
PropertyValueUIService
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing.Design;
namespace PropertyGridDataBindingGlyph
{
public class PropertyValueUIService : IPropertyValueUIService
{
private PropertyValueUIHandler handler;
private ArrayList items;
public event EventHandler PropertyUIValueItemsChanged;
public void NotifyPropertyValueUIItemsChanged()
{
PropertyUIValueItemsChanged?.Invoke(this, EventArgs.Empty);
}
public void AddPropertyValueUIHandler(PropertyValueUIHandler newHandler)
{
handler += newHandler ?? throw new ArgumentNullException("newHandler");
}
public PropertyValueUIItem[] GetPropertyUIValueItems(ITypeDescriptorContext context, PropertyDescriptor propDesc)
{
if (propDesc == null)
throw new ArgumentNullException("propDesc");
if (this.handler == null)
return new PropertyValueUIItem[0];
lock (this)
{
if (this.items == null)
this.items = new ArrayList();
this.handler(context, propDesc, this.items);
int count = this.items.Count;
if (count > 0)
{
PropertyValueUIItem[] propertyValueUiItemArray = new PropertyValueUIItem[count];
this.items.CopyTo((Array)propertyValueUiItemArray, 0);
this.items.Clear();
return propertyValueUiItemArray;
}
}
return null;
}
public void RemovePropertyValueUIHandler(PropertyValueUIHandler newHandler)
{
handler -= newHandler ?? throw new ArgumentNullException("newHandler");
}
}
}
ExPropertyGrid
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using System.Windows.Forms.Design;
namespace PropertyGridDataBindingGlyph
{
public class ExPropertyGrid : PropertyGrid
{
Bitmap dataBitmap;
public ExPropertyGrid()
{
dataBitmap = new Bitmap(typeof(ControlDesigner).Assembly
.GetManifestResourceStream("System.Windows.Forms.Design.BoundProperty.bmp"));
dataBitmap.MakeTransparent();
}
protected override void OnSelectedObjectsChanged(EventArgs e)
{
base.OnSelectedObjectsChanged(e);
this.BeginInvoke(new Action(() => { ShowGlyph(); }));
}
protected override void OnPropertySortChanged(EventArgs e)
{
base.OnPropertySortChanged(e);
this.BeginInvoke(new Action(() => { ShowGlyph(); }));
}
private void ShowGlyph()
{
var grid = this.Controls[2];
var field = grid.GetType().GetField("allGridEntries",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance | BindingFlags.FlattenHierarchy);
var value = field.GetValue(grid);
if (value == null)
return;
var entries = (value as IEnumerable).Cast<GridItem>().ToList();
if (this.SelectedObject is Control)
{
((Control)this.SelectedObject).DataBindings.Cast<Binding>()
.ToList().ForEach(binding =>
{
var item = entries.Where(x => x.PropertyDescriptor?.Name == binding.PropertyName).FirstOrDefault();
var pvSvcField = item.GetType().GetField("pvSvc", BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.FlattenHierarchy);
IPropertyValueUIService pvSvc = new PropertyValueUIService();
pvSvc.AddPropertyValueUIHandler((context, propDesc, valueUIItemList) =>
{
valueUIItemList.Add(new PropertyValueUIItem(dataBitmap, (ctx, desc, invokedItem) => { }, GetToolTip(binding)));
});
pvSvcField.SetValue(item, pvSvc);
});
}
}
private static string GetToolTip(Binding binding)
{
var value = "";
if (binding.DataSource is ITypedList)
value = ((ITypedList)binding.DataSource).GetListName(new PropertyDescriptor[] { });
else if (binding.DataSource is Control)
value = ((Control)binding.DataSource).Name;
else if (binding.DataSource is Component)
value = ((Component)binding.DataSource).Site?.Name;
if (string.IsNullOrEmpty(value))
value = "(List)";
return value + " - " + binding.BindingMemberInfo.BindingMember;
}
}
}
Related
In Visual Studio 2022, in designer, on a form, I have many different types of controls like buttons, labels and others.
How do I select all buttons, for instance?
What I am doing now is pressing Shift key and then selecting the buttons one by one, which is taking to much time.
You can create a new Visual Studio extension which adds a new menu, or a new toolbar, or a new button to an existing window. Then using IDesignerHost and INestedContainer, get the selectable components, and filter them to get list of buttons, then using ISelectionService select them.
You can find all the building blocks of the solution here and there, for example in my following posts:
Get all controls of the current form at design-time
Add a button to solution explorer to open .cs files in code editor
But to keep it simple to use and simple for test, I'll use a different solution that I used here, which is adding a new designer verb to context menu, using a base class.
To do so, Create a BaseForm class deriving from Form. Then each form which drives from this class will have above functionality that you see in the animation. Here is the code of the base form:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.Design;
public partial class BaseForm : Form
{
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (DesignMode && Site != null)
{
var host = Site.GetService(typeof(IDesignerHost)) as IDesignerHost;
if (host != null)
{
var designer = (DocumentDesigner)host.GetDesigner(this);
if (designer != null)
{
designer.ActionLists.Clear();
designer.ActionLists.Add(
new MyActionList(host, designer));
}
}
}
}
public class MyActionList : DesignerActionList
{
IDesignerHost host;
ControlDesigner designer;
public MyActionList(IDesignerHost host,
ControlDesigner designer) : base(designer.Component)
{
this.host = host;
this.designer = designer;
}
private void SelectAllButtons()
{
var buttons = GetSelectableComponents(host).OfType<Button>().ToList();
var svc = host.GetService(typeof(ISelectionService)) as ISelectionService;
if (svc != null)
{
svc.SetSelectedComponents(buttons);
}
}
public override DesignerActionItemCollection GetSortedActionItems()
{
var items = new DesignerActionItemCollection();
var category = "New Actions";
items.Add(new DesignerActionMethodItem(this, "SelectAllButtons",
"Select all buttons", category, true));
return items;
}
private List<IComponent> GetSelectableComponents(IDesignerHost host)
{
var components = host.Container.Components;
var list = new List<IComponent>();
foreach (IComponent c in components)
list.Add(c);
for (var i = 0; i < list.Count; ++i)
{
var component1 = list[i];
if (component1.Site != null)
{
var service = (INestedContainer)component1.Site.GetService(
typeof(INestedContainer));
if (service != null && service.Components.Count > 0)
{
foreach (IComponent component2 in service.Components)
{
if (!list.Contains(component2))
list.Add(component2);
}
}
}
}
return list;
}
}
}
I have a ToolStripSplitButton with various elements in dropdown list.
One of them is a Trackbar enclosed in a ToolStripControlHost, called ToolStripTrackbarItem. It's code (I've got it from stackoverflow):
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;
namespace Application
{
[System.ComponentModel.DesignerCategory("code")]
[System.Windows.Forms.Design.ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ContextMenuStrip | ToolStripItemDesignerAvailability.MenuStrip)]
public class ToolStripTrackbarItem : ToolStripControlHost
{
public ToolStripTrackbarItem()
: base(CreateControlInstance())
{
this.Size = Control.Size;
}
public TrackBar TrackBar
{
get { return Control as TrackBar; }
}
private static Control CreateControlInstance()
{
TrackBar t = new TrackBar();
t.AutoSize = false;
return t;
}
[DefaultValue(0)]
public int Value
{
get { return TrackBar.Value; }
set { TrackBar.Value = value; }
}
protected override void OnSubscribeControlEvents(Control control)
{
base.OnSubscribeControlEvents(control);
TrackBar trackBar = control as TrackBar;
trackBar.ValueChanged += new EventHandler(trackBar_ValueChanged);
}
protected override void OnUnsubscribeControlEvents(Control control)
{
base.OnUnsubscribeControlEvents(control);
TrackBar trackBar = control as TrackBar;
trackBar.ValueChanged -= new EventHandler(trackBar_ValueChanged);
}
void trackBar_ValueChanged(object sender, EventArgs e)
{
if (this.ValueChanged != null)
ValueChanged(sender, e);
}
public event EventHandler ValueChanged;
protected override Size DefaultSize
{
get { return new Size(300, 16); }
}
}
It works, but I need to show images to the left of the dropdown items:
I'm successful with a simple ToolStripMenuItem by setting the Image property. However, it is ineffective to set Image property of my ToolStripTrackbarItem (that is inherited from ToolStripControlHost, see code above). According to MSDN, Image property is irrelevant to ToolStripControlHost.
What does it mean? Is it not even possible to include an image left to ToolStripControlHost?
If it is possible anyway, how to do that?
You should solve 2 problems here:
ToolStripControlHost doesn't show Image property and also doesn't serialize the image when you save the form.
ToolStripProfessionalRendered doesn't draw image for ToolStripControlHost.
You need to override Image property of ToolStripControlHost and make it browsable and serializable. Also you need to create a custom renderer to draw the image in the correct location and size. Then if you simply set the renderer for ToolStrip using below code, you will get expected result:
this.toolStrip1.Renderer = new MyCustomRenderer();
ToolStripTrackBar
The item enables Image property to show in property grid and let it serialize when saving form.
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.Design;
[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.All)]
public class ToolStripTrackBar : ToolStripControlHost
{
public TrackBar TrackBar { get { return (TrackBar)Control; } }
public ToolStripTrackBar() : base(CreateControl()) { }
private static TrackBar CreateControl()
{
var t = new TrackBar()
{ TickStyle = TickStyle.None, AutoSize = false, Height = 28 };
return t;
}
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public override Image Image
{
get { return base.Image; }
set { base.Image = value; }
}
/*Expose properties and events which you need.*/
public int Value
{
get { return TrackBar.Value; }
set { TrackBar.Value = value; }
}
}
MyCustomRenderer
This renderer draws images for ToolStripTrackBar.
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public class MyCustomRenderer : ToolStripProfessionalRenderer
{
protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
{
base.OnRenderImageMargin(e);
e.ToolStrip.Items.OfType<ToolStripTrackBar>()
.ToList().ForEach(item =>
{
if (item.Image != null)
{
var size = item.GetCurrentParent().ImageScalingSize;
var location = item.Bounds.Location;
location = new Point(5, location.Y + 1);
var imageRectangle = new Rectangle(location, size);
e.Graphics.DrawImage(item.Image, imageRectangle,
new Rectangle(Point.Empty, item.Image.Size),
GraphicsUnit.Pixel);
}
});
}
}
I have created a new User Control that has a property like...
private Font m_DisplayFont;
public Font DisplayFont
{
get { return m_DisplayFont; }
set { m_DisplayFont = value; }
}
I want to set m_DisplayFont to the parent's font when I drop the new User Control into a container (Form, GroupBox, etc).
I currently have tried the following but can not get the parent when the class is constructed. Any suggested would be welcome. Thanks!
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Reflection;
namespace MyTestControl
{
public partial class UserControl1 : ProgressBar
{
private Font m_DisplayFont;
public Font DisplayFont
{
get { return m_DisplayFont; }
set { m_DisplayFont = value; }
}
public UserControl1()
{
InitializeComponent();
object parent = base.Parent;
m_DisplayFont = null;
if (parent != null)
{
//See if parent contains a font
Type type = parent.GetType();
IList<PropertyInfo> props = new List<PropertyInfo>(type.GetProperties());
foreach (PropertyInfo propinfo in props)
{
if (propinfo.Name == "Font")
{
m_DisplayFont = (Font)propinfo.GetValue(parent, null);
}
}
}
if (m_DisplayFont == null) m_DisplayFont = new Font("Verdana", 20.25f);
}
}
}
You can use the ParentChanged event:
Occurs when the Parent property value changes.
private void ParentChanged(Object sender, EventArgs args)
{
var parent = this.Parent;
if (parent == null)
return;
var fontProp = parent
.GetType()
.GetProperty("Font");
var font = (fontProp == null) ?
new Font("Verdana", 20.25f) : (Font)fontProp.GetValue(parent, null);
this.m_DisplayFont = font;
}
This question already has answers here:
DependencyProperty getter/setter not being called
(2 answers)
Closed 7 years ago.
I'm using MVVMLight. This is what I have from an example here.
private ObservableCollection<Inline> _inlineList;
public ObservableCollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
private void SendClicked()
{
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
}
public class BindableTextBlock : TextBlock
{
public ObservableCollection<Inline> InlineList
{
get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ObservableCollection<Inline>)e.NewValue);
}
}
<testRobot:BindableTextBlock
Width="Auto" Height="Auto"
InlineList="{Binding InlineList}" >
</testRobot:BindableTextBlock>
The problem is that bound property InlineList never gets updated. I don't see any text I add to the collection ObservableCollection. When I put a break point in OnPropertyChanged method it never gets hit. I know my data context is set correctly as other bound controls work.
What could be the problem?
Ok you only need to add these in your BindableTextBlock With your Original solution. What we do here is we add handler for when collection is changed (meaning new values are added), we only do that when the collection is set. So with the binding that you have in your xaml every change you make on the collection in the VM fires collection changed event in the textblock which in turn just appends values to the Inline.
private void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
Inlines.AddRange(e.NewItems);
}
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.InlineList.CollectionChanged += textBlock.CollectionChangedHandler;
}
BEFORE EDIT for history reasons
Ok I saw what's happening so first the explanation then an example.
So first some basic concepts about wpf:
In order to have your view notified for a change in bound variable in your ViewModel (or whatever that is DataContext at the moment) you have to either RaisePropertyChanged event with the name of the changed property or use something that's doing this somehow :) - like ObservableCollection.
So some use cases:
You have a property with field -> it is common practice to have it like this:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get
{
return _inlineList;
}
set
{
_inlineList = value;
RaisePropertyChanged("InlineList");
}
}
This ensures that when you set a new value to InlineList the view will be notified
Or in your case what I've used:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
If you check the description of Set method you'll see that it is setting the value and raising the property (and some more stuff)
You want to have automatic updates and use ObservableCollection -> I use it like this:
private ObservableCollection<ClientFilter> clientFilters;
public IEnumerable<ClientFilter> ClientFilters
{
get
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
return this.clientFilters;
}
set
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
SetObservableValues<ClientFilter>(this.clientFilters, value);
}
}
The method SetObservableValues is in my main ViewModel and is doing this:
public static void SetObservableValues<T>(
ObservableCollection<T> observableCollection,
IEnumerable<T> values)
{
if (observableCollection != values)
{
observableCollection.Clear();
foreach (var item in values)
{
observableCollection.Add(item);
}
}
}
This method ensures that if the reference to the obs collection is not the same it will clear the old one and reuse it, because when you bind you bind to the reference at common mistake is to then change the reference itself not the values, which in turn doesn't update anything on the UI and you think binding is broken :)
So If you want it to function normally you just add/remove to the Collection/Enumerable ClientFilters
Now the solution
So I'm not 100% sure what you want to achieve but here's what you could do in order to have your binding working
Your ViewModel
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
namespace WpfApplication3.ViewModel
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
InlineList = new List<Inline>
{
new Run("This is some bold text") { FontWeight = FontWeights.Bold },
new Run("Some more text"),
new Run("This is some text") { TextDecorations = TextDecorations.Underline }
};
});
}
}
}
}
Your custom control -> BindableTextBlock
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace WpfApplication3
{
public class BindableTextBlock : TextBlock
{
public ICollection<Inline> InlineList
{
get { return (ICollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(List<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ICollection<Inline>)e.NewValue);
}
}
}
Your XAML
On your Page or Window (depending on platform)
DataContext="{Binding Main, Source={StaticResource Locator}}"
Then inside
<StackPanel>
<Button Command="{Binding SendClicked}">SendClicked</Button>
<local:BindableTextBlock Background="Black" Foreground="AliceBlue"
Width = "Auto" Height="Auto"
InlineList="{Binding InlineList}"
>
</local:BindableTextBlock>
</StackPanel>
All assuming you have your ViewModelLocator from MVVM Light register your view model
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
namespace WpfApplication3.ViewModel
{
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
}
}
}
ALTERNATIVE
Alternatively you could have your command like this:
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
_inlineList = new List<Inline>();
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
RaisePropertyChanged("InlineList");
});
}
}
But you have to use the other option of defining the property as described at the beginning of my post.
You could do it in other ways of course.
Just one more advice -> It is considered bad practice and not in the spirit of MVVM to have UI elements in your view model, so change in architecture is strongly advised in this code IMO.
Post got too long (as usual), if you need aditional explanation please let me know.
Cheers and happy coding ;)
What is the easiest way to have Visual Studio-like editor for string in PropertyGrid? For example in Autos/Locals/Watches you can preview/edit string values in-line but you can also click on magnifying glass and see string in external window.
You can do this via a UITypeEditor, as below. Here I'm using it on an individual property, but IIRC you can also subvert all strings (so that you don't need to decorate all the properties):
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using(var frm = new Form { Controls = { new PropertyGrid {
Dock = DockStyle.Fill, SelectedObject = new Foo { Bar = "abc"}}}})
{
Application.Run(frm);
}
}
}
class Foo
{
[Editor(typeof(FancyStringEditor), typeof(UITypeEditor))]
public string Bar { get; set; }
}
class FancyStringEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
var svc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (svc != null)
{
using (var frm = new Form { Text = "Your editor here"})
using (var txt = new TextBox { Text = (string)value, Dock = DockStyle.Fill, Multiline = true })
using (var ok = new Button { Text = "OK", Dock = DockStyle.Bottom })
{
frm.Controls.Add(txt);
frm.Controls.Add(ok);
frm.AcceptButton = ok;
ok.DialogResult = DialogResult.OK;
if (svc.ShowDialog(frm) == DialogResult.OK)
{
value = txt.Text;
}
}
}
return value;
}
}
To apply this for all string members: instead of adding the [Editor(...)], apply the following somewhere early in the app:
TypeDescriptor.AddAttributes(typeof(string), new EditorAttribute(
typeof(FancyStringEditor), typeof(UITypeEditor)));