Dynamic Data Display graph of variable number of rectangles - c#

I'm trying to plot a user's input data that will eventually become a series of rectangles (different sizes and positions, not overlapping) in a graph. All examples I've read either only plot lines with a variable number of points or hard-code in the shapes in the XAML. But I don't know how many rectangles the data will need. My ideal case would be to follow MVVM and simply bind from the XAML to an ObservableCollection I can modify, but most examples I see seem to use the code-behind instead, accessing the ChartPlotter directly. Here's what I've got with some simple rectangles drawn and one modified, which works:
VisualizationWindow.xaml
<Window x:Class="View.VisualizationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
Title="VisualizationWindow" MinHeight="300" MinWidth="500" Height="300" Width="500">
<Grid>
<d3:ChartPlotter Name="Chart">
<d3:RectangleHighlight Name="Rect1" Bounds="-1,-1.5,.5,2" StrokeThickness="3" Fill="Blue" ToolTip="Blue!"></d3:RectangleHighlight>
<d3:RectangleHighlight Name="Rect2" Bounds="1,1.5,2,.5" StrokeThickness="1" Fill="Red" ToolTip="Red!"></d3:RectangleHighlight>
</d3:ChartPlotter>
</Grid>
</Window>
VisualizationWindow.xaml.cs
public partial class VisualizationWindow : Window
{
public VisualizationWindow(ListViewModel vm)
{
InitializeComponent();
this.DataContext = vm;
Chart.Viewport.Visible = new Rect(-1, -1, .5, .5);
Rect1.Bounds = new Rect(0,0,.3,.3);
}
}
Documentation on Dynamic Data Display is almost nonexistent. I'd love to know if other libraries can do this more easily if D3 can't do it elegantly.

Adding ItemsSource handling to existing classes.
It seems like they've done everything to make ChartPlotter only accept IPlotterElements. There's no ItemsSource property, so Children will always return the actual elements. RectangleHighlight's Bounds property is not bindable and the class is sealed, barring any methods to override the property.
We can derive from the class to "inject" ItemsSource handling. It won't work like the real deal, as there's no non-hackish way to have the Children property reflect the data-binding. But, we can still assign ItemsSource this way.
We'll need a few things. We'll need the actual ItemsSource property. A way to react to it being set. And if we want binding to dataObjects, a way to handle DataTemplates. I didn't dig into the existing source code yet. However, I did come up with a way to Handle DataTemplates without DataTemplateSelector. But it also won't work with DataTemplateSelector unless you modify my examples.
This answer assumes you know how binding works, so we'll skip to the initial classes without much hand-holding.
Xaml first:
<local:DynamicLineChartPlotter Name="Chart" ItemsSource="{Binding DataCollection}">
<local:DynamicLineChartPlotter .Resources>
<DataTemplate DataType{x:Type local:RectangleHighlightDataObject}>
<d3:RectangleHighlight
Bounds="{Binding Bounds}"
StrokeThickness="{Binding StrokeThickness}"
Fill="{Binding Fill}"
ToolTip="{Binding ToolTip}"
/>
</DataTemplate>
</local:DynamicLineChartPlotter .Resources>
</local:DynamicLineChartPlotter >
Classes:
public class RectangleHighlightDataObject
{
public Rect Bounds { get; set; }
public double StrokeThickness { get; set; }
public Brush Fill { get; set; }
public String ToolTip { get; set; }
}
public class VisualizationWindow
{
public VisualizationWindow()
{
DataCollection.Add(new RectangleHighlightDataObject()
{
Bounds = new Rect(-1,-1.5,.5,2),
StrokeThickness = 3,
Fill = Brushes.Blue,
ToolTip = "Blue!"
});
DataCollection.Add(new RectangleHighlightDataObject()
{
Bounds = new Rect(1,1.5,2,.5),
StrokeThickness = 1,
Fill = Brushes.Red,
ToolTip = "Red!"
});
}
public ObservableCollection<RectangleHighlightDataObject> DataCollection =
new ObservableCollection<RectangleHighlightDataObject>();
}
You'll have to use a derived class from ChartPlotter than implements an ItemsSource.
An example gleaned from a discussion on how to implement dynamic D3 types. I modified to use DataTemplates instead of actual object elements, this is to support databinding par the OP.
public class DynamicLineChartPlotter : Microsoft.Research.DynamicDataDisplay.ChartPlotter
{
public static DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource",
typeof(IEnumerable),
typeof(DynamicLineChartPlotter),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Bindable(true)]
public IEnumerable ItemsSource
{
get
{
return (IEnumerable)GetValue(ItemsSourceProperty);
}
set
{
if (value == null)
ClearValue(ItemsSourceProperty);
else
SetValue(ItemsSourceProperty, value);
}
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
IEnumerable oldValue = (IEnumerable)e.OldValue;
IEnumerable newValue = (IEnumerable)e.NewValue;
if (e.OldValue != null)
{
control.ClearItems();
}
if (e.NewValue != null)
{
control.BindItems((IEnumerable)e.NewValue);
}
}
private void ClearItems()
{
Children.Clear();
}
private void BindItems(IEnumerable items)
{
foreach (var item in items)
{
var template = GetTemplate(item);
if (template == null) continue;
FrameworkElement obj = template.LoadContent() as FrameworkElement;
obj.DataContext = item;
Children.Add((IPlotterElement)obj);
}
}
private DataTemplate GetTemplate(object item)
{
foreach (var key in this.Resources.Keys)
{
if (((DataTemplateKey)key).DataType as Type == item.GetType())
{
return (DataTemplate)this.Resources[key];
}
}
return null;
}
}
Now this is where you hit a brick wall.
RectangleHighlight Bounds property cannot be data-bound. You also can't derive from them to get around this problem.
We could hack our way around by pulling out the data template and generating a static RectangleHighlight, but if the data values change, we're sol.
So, how to fix this?
Well, we could use attached properties!
Using Attached Properties
We'll create a static class that will handle the attached property. It responds to the OnPropertyChanged to manually create and set the real property. Now, this will only work one way. If you happen to change the property, it won't update the attached property. However, this shouldn't be a problem since we should only ever update our data object.
Add this class
public class BindableRectangleBounds : DependencyObject
{
public static DependencyProperty BoundsProperty = DependencyProperty.RegisterAttached("Bounds", typeof(Rect), typeof(BindableRectangleBounds), new PropertyMetadata(new Rect(), OnBoundsChanged));
public static void SetBounds(DependencyObject dp, Rect value)
{
dp.SetValue(BoundsProperty, value);
}
public static void GetBounds(DependencyObject dp)
{
dp.GetValue(BoundsProperty);
}
public static void OnBoundsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
var property = dp.GetType().GetProperty("Bounds");
if (property != null)
{
property.SetValue(dp, args.NewValue, null);
}
}
}
Then change the XAML line from
Bounds="{Binding Bounds}"
to
local:BindableRectangleBounds.Bounds="{Binding Bounds}"
Responding to collection changed.
So far so good. But the OP noticed that if he made changes to the collection he assigned ItemsSource to, nothing changes in the control. Well that's because we only add children when ItemsSource is assigned to. Now, barring knowing exactly how ItemsControl implements ItemsSource. I know we can get around this by registering to the ObservableCollection's events when the collection changes. I put a simple method of rebinding the controls whenever the collection changes. This will only work if ItemsSource is assigned by an ObservableCollection though. But I would think this would have the same problem with ItemsControl not having an ObservableCollection.
We're still not reassigning the dataContext however. So don't expect altering Children would correctly rebind. However if you did alter children directly, instead of the ItemsSource in an ItemsControl, you'd lose the binding. So I may very well be on track. The only quirk we lose is that ItemsControl returns the ItemsSource when you refer to Children after setting ItemsSource. I haven't worked around this yet. The only thing I can think of offhand is to hide the children property, but that's not a nice thing to do, and won't work if you refer to the control as a ChartPlotter.
Add the following
public DynamicLineChartPlotter()
{
_HandleCollectionChanged = new NotifyCollectionChangedEventHandler(collection_CollectionChanged);
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
IEnumerable oldValue = (IEnumerable)e.OldValue;
IEnumerable newValue = (IEnumerable)e.NewValue;
INotifyCollectionChanged collection = e.NewValue as INotifyCollectionChanged;
INotifyCollectionChanged oldCollection = e.OldValue as INotifyCollectionChanged;
if (e.OldValue != null)
{
control.ClearItems();
}
if (e.NewValue != null)
{
control.BindItems((IEnumerable)e.NewValue);
}
if (oldCollection != null)
{
oldCollection.CollectionChanged -= control._HandleCollectionChanged;
control._Collection = null;
}
if (collection != null)
{
collection.CollectionChanged += control._HandleCollectionChanged;
control._Collection = newValue;
}
}
void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ClearItems();
BindItems(_Collection);
}
NotifyCollectionChangedEventHandler _HandleCollectionChanged;
IEnumerable _Collection;

Related

nested property propertychanged event only fired once in usercontrol in silverlight

There are two Viewmodels, both of them had implemented the INotifyPropertyChanged interface (I have called the OnpropertyChanged("propertyname") in my actual code).
Public Class A{
public B BProperty
{
get
{
return _BProperty;
}
set
{
if (_BProperty != null)
_BProperty.PropertyChanged -= _BProperty_PropertyChanged;
_BProperty = value;
OnPropertyChanged("BProperty");
if (_BProperty != null)
_BProperty.PropertyChanged += _BProperty_PropertyChanged;
}
}
void _BProperty_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "status")
{
OnPropertyChanged("BProperty");
}
}
B _BProperty;
}
Public Class B
{
public int status{get;set;}
}
I also had a userControl:
<MyUserControl ...
... >
<Grid>
</Grid>
</MyUserControl>
And I had a dependencyProperty:
/// <summary>
/// File status
/// </summary>
public int Filestatus
{
get { return (int)GetValue(FilestatusProperty); }
set { SetValue(FilestatusProperty, value); }
}
public static readonly DependencyProperty FilestatusProperty =
DependencyProperty.Register(
"Filestatus",
typeof(int),
typeof(MyUserControl),
new PropertyMetadata(0, OnFilestatusPropertyChanged));
private static void OnFilestatusPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyUserControl control = (MyUserControl)d;
if (e.NewValue != null)
{
}
}
edit:2015/09/21
Add the get/set methods:
public static readonly DependencyProperty FileStatusProperty = DependencyProperty.RegisterAttached(
"FileStatus", typeof(int), typeof(FileStatusIconControl), new PropertyMetadata(0, PropertyChangedCallback));
public static int GetFileStatus(DependencyObject source)
{
return (int)source.GetValue(FileStatusProperty);
}
public static void SetFileStatus(DependencyObject target, int value)
{
target.SetValue(FileStatusProperty, value);
}
private static void PropertyChangedCallback(
DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
FileStatusIconControl fsic = dependencyObject as FileStatusIconControl;
if(fsic != null)
wahahahahaha;
}
edit end
I used this userControl in my mainPage like this:
<mainPage ...
...>
<Grid>
<MyUserControl Filestatus={Binding Bproperty.status} />
<TextBox Text={Binding Bproperty.status} />
</Grid>
</mainPage>
The datacontext of mainPage is an instance of Class A.
My question is:
When status is changed, the text of textbox is changed, but the OnFilestatusPropertyChanged method only was called once when Usercontrol is loaded. Why?
Thanks.
I will start by saying that while I was looking into your question I ran into some problems with the code you have provided. I appreciate that somewhere you have some real code which has a problem and you cannot share this code with us so have tried to reproduce the problem in a smaller set of files.
However, if you do do this, please at least verify that the code you have provided runs and exhibits the problem. It is evident that you haven't tried to run your sample code (particularly the XAML) as there are problems with it:
attribute values should be surrounded in double-quotes,
the binding path Bproperty.status should be BProperty.status (first P capitalised).
All these things slow down someone trying to help you. Worse still, when I do find a problem with your code I can't be sure whether it's a genuine problem that your real code also has or whether it's something you introduced putting together your sample code. So all I can do is point out all the problems I find in the hope that one of them is the problem you have in your real code.
Firstly, your TextBox's Text property binding doesn't contain Mode=TwoWay. In WPF, this binding is TwoWay by default, but in Silverlight all bindings are OneWay by default. If you are familiar with WPF, this may be why you omitted Mode=TwoWay.
Secondly, I don't see why you have implemented class B as you have, apparently leaving it up to class A to fire property-changed events on its behalf. This approach doesn't work: when Silverlight updates the value in the status property of a B instance, it does so by calling the status setter of the B instance. Your B class has only an autogenerated property setter, which certainly doesn't fire the PropertyChanged event. Because this event wasn't fired, Silverlight doesn't know that is has some updates to do, and furthermore your A class isn't aware that it has changed either.
I would implement INotifyPropertyChanged in the usual way in class B, by calling OnPropertyChanged in the status setter. I would also remove the BProperty_PropertyChanged event handler in class A as I don't think it does you any favours.

Delegating UI actions to ViewModel for execution

I have an ScrollViewer which holds an ItemsSource. The items are bound to the ViewModel and are sorted in ascending order. The users are allowed to add items to this list, however, the list needs to be scrolled to bottom because of the sorting order. From what I found, ScrollViewer doesn't have a "lock to bottom" feature, but has a ScrollToEnd method which does what I'm looking for.
The problem though, is that the items are added in the ViewModel, and the ViewModel obviously doesn't have access to the View to call the ScrollToEnd method on the ScrollViewer. To get around this, I declared an action delegate in the ViewModel like this:
public Action ScrollAction { get; set; }
And set it in the View upon creation of the ViewModel:
viewModel.ScrollAction = () => scrollViewer.ScrollToEnd();
The delegate is executed in the ViewModel once an item is added to the list. Even though this works, it feels a little hacky to me, since this kind of breaks the isolation of the ViewModel from the View. Is there a better way to achieve this?
I would also vote for an AttachedProperty to your scroll viewer.
I created following attached property to bind scroll to end with a boolean variable.
public static class ScrollViewerBehavior
{
public static readonly DependencyProperty ScrollToRightEndProperty =
DependencyProperty.RegisterAttached("ScrollToRightEnd", typeof (bool), typeof (ScrollViewerBehavior),
new PropertyMetadata(false, OnScrollToRightEndPropertyChanged));
public static bool GetScrollToRightEnd(DependencyObject obj)
{
return (bool) obj.GetValue(ScrollToRightEndProperty);
}
public static void SetScrollToRightEnd(DependencyObject obj, bool value)
{
obj.SetValue(ScrollToRightEndProperty, value);
}
private static void OnScrollToRightEndPropertyChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var sv = sender as ScrollViewer;
if (sv == null) return;
if ((bool) e.NewValue)
{
sv.ScrollToRightEnd();
}
else
{
sv.ScrollToLeftEnd();
}
}
}
You can use this attached property in your XAML...
<ScrollViewer ... local:ScrollViewerBehavior.ScrollToRightEnd="{Binding IsScrolledToEnd}" ... />
Alternatively if you want to save the action delegate as in your question, you could do the following in the OnScrollToRightEndPropertyChanged method above.
.....
var viewModel = sv.DataContext as YourViewModel;
if (viewModel != null)
{
viewModel.ScrollAction = () => sv.ScrollToRightEnd();
}
.....

Set DataTemplate in template selector as dynamic resource

I have a control in which I need to set data template based on various conditions so I decided to use a DataTemplateSelector which selects templates from resources of the control its being assigned to.
This works, but here is a catch: I am reloading these resources from file (when there is file system change) and I need to update already rendered controls with the new template. This would work if I simply used DynamicResource instead of selector.
Selector looks something like this:
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
//complex rules that select the template are here
//this unfortunately sets the template statically - if it changes, it won't get updated
return template;
}
So if the resources change, the selector is never reevaluated as it would be if I used DynamicResource.
I had an idea to solve this: select the template in ViewModel, so that when resources change, I can update my DataTemplate property.
My attempt of ViewModel (simplified example, it implements INotifyPropertyChange properly):
class MyViewModel {
public DataTemplate DataTemplate {get;set;}
public MyModel Model {
get {return _model;}
set {
if(_model != value) {
_model = value;
//Select new template here
//DUH: how do I access the resources as I would in DataTemplateSelector, when I don't have access to the container parameter?
}
}
}
}
I am pretty sure that I am doing this the wrong way, but how to do it properly? I don't want to access the resources from some hard-coded static location for various reasons. I really need to find them in the container it is being assigned to.
I know the question is confusing, so feel free to ask and I will try to clarify.
So after long hours of trying to figure this out using various hackish methods, it showed to be really easily solvable problem.
We set our data template (only key to data template in fact) in view-model and then apply the template in simple attached property.
xaml:
<ContentControl Content="{Binding Content}" local:ContentTemplate.ContentTemplateKey="{Binding TemplateKey}">
<!-- Some other stuff -->
</ContentControl>
attached property:
public static class ContentTemplate
{
public static object GetContentTemplateKey(DependencyObject obj)
{
return (object)obj.GetValue(ContentTemplateKeyProperty);
}
public static void SetContentTemplateKey(DependencyObject obj, object value)
{
obj.SetValue(ContentTemplateKeyProperty, value);
}
public static readonly DependencyProperty ContentTemplateKeyProperty = DependencyProperty.RegisterAttached("ContentTemplateKey", typeof(object), typeof(ContentTemplate), new UIPropertyMetadata(null, OnContentTemplateKeyChanged));
private static void OnContentTemplateKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var key = e.NewValue;
var element = d as FrameworkElement;
if (element == null)
return;
element.SetResourceReference(ContentControl.ContentTemplateProperty, key);
}
}
binding object if resource uses x:Key="ResourceName":
new
{
Content = something,
TemplateKey = "ResourceName",
}
binding object if resource uses TargetType="{x:Type Person}":
new
{
Content = something,
TemplateKey = new DataTemplateKey(typeof(Person)),
}
Of course the binding object should implement INotifyPropertyChange so the templates update on the fly.

ListBox bound to ObservableCollection doesn't update

I am having trouble getting a ListBox binding to work as expected. I'm currently attempting to bind a ListBox to a singleton exposed ObservableCollection of items. The items are a separate class themselves. Currently, I am binding like this:
<Window x:Class="toxySharp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:classes="clr-namespace:MyApplication.Classes"
Title="MainWindow" Height="325" Width="400"
DataContext="{Binding Source={x:Static local:SingletonClass.Instance}}">
<Grid x:Name="LayoutRoot">
<ListBox x:Name="lstMyList" ItemsSource="{Binding Path=Objects, Mode=TwoWay}" DisplayMemberPath="Name" />
</Grid>
</Window>
My singleton is a basic implementation like this:
public class SomeObject : INotifyPropertyChanged
{
private Int32 m_vId;
private String m_vName;
public SomeObject() { }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public Int32 Id
{
get { return this.m_vId; }
set { this.m_vId = value; NotifyPropertyChanged("Id"); }
}
public String Name
{
get { return this.m_vName; }
set { this.m_vName = value; NotifyPropertyChanged("Name"); }
}
}
public class SingletonClass : INotifyPropertyChanged
{
private static SingletonClass m_vInstance;
private ObservableCollection<SomeObject> m_vObjects;
private SingletonClass()
{
this.m_vObjects = new ObservableCollection<SomeObject>();
for (int x = 0; x < 255; x++)
this.m_vObjects.Add(new SomeObject() { Id = x, Name = String.Format("{0} - new object", x) });
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public static SingletonClass Instance
{
get
{
if (m_vInstance == null)
m_vInstance = new SingletonClass();
return m_vInstance;
}
}
public ObservableCollection<SomeObject> Objects
{
get { return this.m_vObjects; }
set { this.m_vObjects = value; NotifyPropertyChanged("Objects"); }
}
}
Currently, the binding works on startup. The application will bind and properly show the names of each object. For example this is a test app doing the same implementation:
In my main actual application I have async methods being called (Socket stuff BeginConnect, BeginSend, etc.) that use callbacks that can update the collection. (It's a list of players so when certain packets are received the list is updated with their data.)
My problem is when the collection is updated inside one of the async callbacks it doesn't update on the list. The collection data is updated properly, setting a break in the main code anywhere shows the collection being updated but the listbox never updates to reflect the changes. So it just stays saying the same thing no matter what.
Did I overlook something?
I've tried using a CollectionViewSource as well to allow filtering and that has the same problem.
== EDIT ==
I've found the problem which lies in the singleton with how the collection is initialized. Instead of using the internal copy member when initializing the collection, I needed to use the exposed property to allow it to update the UI.
So using the following fixed it:
private SingletonClass()
{
this.Objects = new ObservableCollection<SomeObject>();
for (int x = 0; x < 255; x++)
this.Objects.Add(new SomeObject() { Id = x, Name = String.Format("{0} - new object", x) });
}
However now that the list binding works I want to be able to filter this based on another property inside the object class. (In the example SomeObject). I have a boolean stating if the object is active. Trying to bind to a CollectionViewSource leads me back to the not updating problems. So is there a way to filter this manually and keep the ui updated?
My problem is when the collection is updated inside one of the async
callbacks it doesn't update on the list.
Well thats the problem isnt it! Observable collections are not thread safe. You need to make them that way.
No TwoWay binding mode or UpdateSourceTrigger=PropertyChanged will help in this case as the problem lies with multi threading in your code...
Use this custom implementation of thread safe and faster observable collection for your ease...
Fast performing and thread safe observable collection
As far as INotifyPropertyChanged interface is concerned, its 'PropertyChangedevent is automatically dispatched to the UI thread. So any multithreaded context updating the properties of a class that implementsINotifyPropertyChanged` will update the GUI.
Let me know if this helps,
UpdateSourceTrigger=PropertyChanged is missing
<ListBox x:Name="lstMyList" ItemsSource="{Binding Path=Objects,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DisplayMemberPath="Name" />

Wrapped WPF Control

I'm trying to create a GUI (WPF) Library where each (custom) control basically wraps an internal (third party) control. Then, I'm manually exposing each property (not all of them, but almost). In XAML the resulting control is pretty straightforward:
<my:CustomButton Content="ClickMe" />
And the code behind is quite simple as well:
public class CustomButton : Control
{
private MyThirdPartyButton _button = null;
static CustomButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton), new FrameworkPropertyMetadata(typeof(CustomButton)));
}
public CustomButton()
{
_button = new MyThirdPartyButton();
this.AddVisualChild(_button);
}
protected override int VisualChildrenCount
{
get
{ return _button == null ? 0 : 1; }
}
protected override Visual GetVisualChild(int index)
{
if (_button == null)
{
throw new ArgumentOutOfRangeException();
}
return _button;
}
#region Property: Content
public Object Content
{
get { return GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
"Content", typeof(Object),
typeof(CustomButton),
new FrameworkPropertyMetadata(new PropertyChangedCallback(ChangeContent))
);
private static void ChangeContent(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
(source as CustomButton).UpdateContent(e.NewValue);
}
private void UpdateContent(Object sel)
{
_button.Content = sel;
}
#endregion
}
The problem comes after we expose MyThirdPartyButton as a property (in case we don't expose something, we would like to give the programmer the means to use it directly). By simply creating the property, like this:
public MyThirdPartyButton InternalControl
{
get { return _button; }
set
{
if (_button != value)
{
this.RemoveVisualChild(_button);
_button = value;
this.AddVisualChild(_button);
}
}
}
The resulting XAML would be this:
<my:CustomButton>
<my:CustomButton.InternalControl>
<thirdparty:MyThirdPartyButton Content="ClickMe" />
</my:CustomButton.InternalControl>
And what I'm looking for, is something like this:
<my:CustomButton>
<my:CustomButton.InternalControl Content="ClickMe" />
But (with the code I have) its impossible to add attributes to InternalControl...
Any ideas/suggestions?
Thanks a lot,
--
Robert
WPF's animation system has the ability to set sub-properties of objects, but the XAML parser does not.
Two workarounds:
In the InternalControl property setter, take the value passed in and iterate through its DependencyProperties copying them to your actual InternalControl.
Use a build event to programmatically create attached properties for all internal control properties.
I'll explain each of these in turn.
Setting properties using the property setter
This solution will not result in the simplified syntax you desire, but it is simple to implement and will probably solve the main problem with is, how to merge values set on your container control with values set on the internal control.
For this solution you continue to use the XAML you didn't like:
<my:CustomButton Something="Abc">
<my:CustomButton.InternalControl>
<thirdparty:MyThirdPartyButton Content="ClickMe" />
</my:CustomButton.InternalControl>
but you don't actually end up replacing your InternalControl.
To do this, your InternalControl's setter would be:
public InternalControl InternalControl
{
get { return _internalControl; }
set
{
var enumerator = value.GetLocalValueEnumerator();
while(enumerator.MoveNext())
{
var entry = enumerator.Current as LocalValueEntry;
_internalControl.SetValue(entry.Property, entry.Value);
}
}
}
You may need some additional logic to exclude DPs not publically visible or that are set by default. This can actually be handled easily by creating a dummy object in the static constructor and making a list of DPs that have local values by default.
Using a build event to create attached properties
This solution allows you to write very pretty XAML:
<my:CustomButton Something="Abc"
my:ThirdPartyButtonProperty.Content="ClickMe" />
The implementation is to automatically create the ThirdPartyButtonProperty class in a build event. The build event will use CodeDOM to construct attached properties for each property declared in ThirdPartyButton that isn't already mirrored in CustomButton. In each case, the PropertyChangedCallback for the attached property will copy the value into the corresponding property of InternalControl:
public class ThirdPartyButtonProperty
{
public static object GetContent(...
public static void SetContent(...
public static readonly DependencyProperty ContentProperty = DependencyProperty.RegisterAttached("Content", typeof(object), typeof(ThirdPartyButtonProperty), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
((CustomButton)obj).InternalControl.Content = (object)e.NewValue;
}
});
}
This part of the implementation is straightforward: The tricky part is creating the MSBuild task, referencing it from your .csproj, and sequencing it so that it runs after the precompile of my:CustomButton so it can see what additional properties it needs to add.

Categories

Resources