I am trying to create a UserControl that will hopefully be able to expose multiple content properties. However, I am ridden with failure!
The idea would be to create this great user control (we'll call it MultiContent) that exposes two content properties so that I could do the following:
<local:MultiContent>
<local:MultiContent.ListContent>
<ListBox x:Name="lstListOfStuff" Width="50" Height="50" />
</local:MultiContent.ListContent>
<local:MultiContent.ItemContent>
<TextBox x:Name="txtItemName" Width="50" />
</local:MultiContent.ItemContent>
</local:MultiContent>
This would be very useful, now I can change ListContent and ItemContent depending on the situation, with common functionality factored out into the MultiContent user control.
However, the way I currently have this implemented, I cannot access the UI elements inside of these content properties of the MultiContent control. For instance, lstListOfStuff and txtItemName are both null when I try to access them:
public MainPage() {
InitializeComponent();
this.txtItemName.Text = "Item 1"; // <-- txtItemName is null, so this throws an exception
}
Here is how I have implemented the MultiContent user control:
XAML: MultiContent.xaml
<UserControl x:Class="Example.MultiContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<ContentControl x:Name="pnlList" Grid.Column="0" />
<ContentControl x:Name="pnlItem" Grid.Column="1" />
</Grid>
</UserControl>
Code Behind: MultiContent.xaml.cs
// Namespaces Removed
namespace Example
{
public partial class MultiContent : UserControl
{
public UIElement ListContent
{
get { return (UIElement)GetValue(ListContentProperty); }
set
{
this.pnlList.Content = value;
SetValue(ListContentProperty, value);
}
}
public static readonly DependencyProperty ListContentProperty =
DependencyProperty.Register("ListContent", typeof(UIElement), typeof(MultiContent), new PropertyMetadata(null));
public UIElement ItemContent
{
get { return (UIElement)GetValue(ItemContentProperty); }
set
{
this.pnlItem.Content = value;
SetValue(ItemContentProperty, value);
}
}
public static readonly DependencyProperty ItemContentProperty =
DependencyProperty.Register("ItemContent", typeof(UIElement), typeof(MultiContent), new PropertyMetadata(null));
public MultiContent()
{
InitializeComponent();
}
}
}
I am probably implementing this completely wrong. Does anyone have any idea how I could get this to work properly? How can I access these UI elements by name from the parent control? Any suggestions on how to do this better? Thanks!
You can definitely achieve your goal but you need to take a different approach.
In your solution you're trying to have a dependency property for of a UIElement - and since it never gets set and default value is null that's why you get a NullReference exception. You could probably go ahead by changing the default value from null to new TextBox or something like that but even if it did work it still would feel like a hack.
In Silverlight you have to implement Dpendency Properties yourself. However you've not implemented them as they should be - I tend to use this dependency property generator to do so.
One of the great things about DPs is that they support change notification. So with that in mind all you have to do to get your sample working is define to DPs: ItemContent and ListContent with the same type as Content (object) and when the framework notifies you that either of them has been changed, simply update your textboxes! So here is the code to do this:
MultiContent.xaml:
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<ContentControl x:Name="pnlList" Grid.Column="0" />
<ContentControl x:Name="pnlItem" Grid.Column="1" />
</Grid>
MultiContent.xaml.cs:
namespace MultiContent
{
public partial class MultiContent : UserControl
{
#region ListContent
/// <summary>
/// ListContent Dependency Property
/// </summary>
public object ListContent
{
get { return (object)GetValue(ListContentProperty); }
set { SetValue(ListContentProperty, value); }
}
/// <summary>
/// Identifies the ListContent Dependency Property.
/// </summary>
public static readonly DependencyProperty ListContentProperty =
DependencyProperty.Register("ListContent", typeof(object),
typeof(MultiContent), new PropertyMetadata(null, OnListContentPropertyChanged));
private static void OnListContentPropertyChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
MultiContent m = sender as MultiContent;
m.OnPropertyChanged("ListContent");
}
#endregion
#region ItemContent
/// <summary>
/// ItemContent Dependency Property
/// </summary>
public object ItemContent
{
get { return (object)GetValue(ItemContentProperty); }
set { SetValue(ItemContentProperty, value); }
}
/// <summary>
/// Identifies the ItemContent Dependency Property.
/// </summary>
public static readonly DependencyProperty ItemContentProperty =
DependencyProperty.Register("ItemContent", typeof(object),
typeof(MultiContent), new PropertyMetadata(null, OnItemContentPropertyChanged));
private static void OnItemContentPropertyChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
MultiContent m = sender as MultiContent;
m.OnPropertyChanged("ItemContent");
}
#endregion
/// <summary>
/// Event called when any chart property changes
/// Note that this property is not used in the example but is good to have if you plan to extend the class!
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Called to invoke the property changed event
/// </summary>
/// <param name="propertyName">The property that has changed</param>
protected void OnPropertyChanged(string propertyName)
{
if (propertyName == "ListContent")
{
// The ListContent property has been changed, let's update the control!
this.pnlList.Content = this.ListContent;
}
if (propertyName == "ItemContent")
{
// The ListContent property has been changed, let's update the control!
this.pnlItem.Content = this.ItemContent;
}
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public MultiContent()
{
InitializeComponent();
}
}
}
MainPage.xaml:
<Grid x:Name="LayoutRoot" Background="White">
<local:MultiContent>
<local:MultiContent.ListContent>
<ListBox x:Name="lstListOfStuff" Width="50" Height="50" />
</local:MultiContent.ListContent>
<local:MultiContent.ItemContent>
<TextBox x:Name="txtItemName" Width="50" />
</local:MultiContent.ItemContent>
</local:MultiContent>
</Grid>
This should do the trick!
Related
So I have a TabControl that has an instance of ViewModel1 as each tab. ViewModel1's View has a custom UserControl I built, that basically exposes a DependencyProperty "Images" that stores the list of images the control has. This property has been bound (OneWayToSource) to ViewModel1's property "Images".
The problem I'm having is that for some reason, all instances of ViewModel1 (all tabs) are sharing this property. So if Tab1 has 1 image in the control, and Tab2 has 3 images in the control, the "Images" property of each ViewModel1 instance has a collection of 4 images.
I don't know how anything like this could happen - anyone have any ideas?
Note that I'm using Caliburn.Micro as a MVVM framework.
EDIT: The property inside the control is defined like this:
public List<ImageData> Images
{
get { return (List<ImageData>)GetValue(ImagesProperty); }
set { return; }
}
public static readonly DependencyProperty ImagesProperty =
DependencyProperty.Register("Images", typeof(List<ImageData>), typeof(WebImageAlbum),
new UIPropertyMetadata(new List<ImageData>()));
New items are just added to that with Images.Add() and inside the View's XAML, this property is bound to the ViewModel's "Images" property with Mode=OneWayToSource.
EDIT: This is what the Tab view with the UserControl looks like:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NET_MD3.Views"
xmlns:cal="http://www.caliburnproject.org"
xmlns:CustomControls="clr-namespace:NET_MD3.CustomControls" x:Class="NET_MD3.Views.AlbumTabView"
mc:Ignorable="d"
d:DesignHeight="267.789" d:DesignWidth="473.684">
<Grid>
<CustomControls:WebImageAlbum x:Name="Album" Margin="10,10,10,50" Width="Auto" Height="Auto" ImageWidthHeight="95"
cal:Message.Attach="[Event ImageClicked] = [Action ImageClicked($eventArgs)]"
Images="{Binding Images, Mode=OneWayToSource}"/>
<Button x:Name="CloseTab" Content="Close tab and delete album" HorizontalAlignment="Left" Margin="10,0,0,10" VerticalAlignment="Bottom" Width="172" Height="30"/>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="342,243,0,0" VerticalAlignment="Top" Width="75" Click="button_Click"/>
</Grid>
This is what the ViewModel looks like:
public class AlbumTabViewModel : Screen, IAlbumTabItem
{
#region Constructor
public AlbumTabViewModel(int id)
{
this.TabID = id;
}
#endregion
#region Properties
/// <summary>
/// Get the Images loaded in this tab's Album (do not use the setter - it's for the Binding only)
/// </summary>
public List<ImageData> Images
{
get; set;
}
/// <summary>
/// The Display name of this Screen (Tab)
/// </summary>
public override string DisplayName
{
get
{
return $"Album #{this.TabID}";
}
set { base.DisplayName = value; }
}
/// <summary>
/// The ID of this tab
/// </summary>
public int TabID { get; private set; }
#endregion
#region Actions
/// <summary>
/// Delete this album tab
/// </summary>
public void CloseTab()
{
this.TryClose();
}
/// <summary>
/// An image has been clicked within the album
/// </summary>
/// <param name="e"></param>
public void ImageClicked(ImageData e)
{
//ttt
}
#endregion
}
Turns out the problem was in the declaration of the DependencyProperty. In the UIPropertyMetadata() that I passed to the static declaration of the property, I made it use a 'new List()' as the default value and turns out that you can't really do that, because it will create that List as a static object, which explains why every instance of the UserControl had the same list. This probably happens because the DependencyProperty itself is static and so when it tries to create that default value, it will be static as well.
I guess the lesson is don't put reference types as default values when declaring a new DependencyProperty, and if you must, set it elsewhere (within the CLR property that accesses the value, or something).
Can anyone show me a simple working example for a WPF MVVM application to set the ItemsSource of combobox B based on the SelectedItem of ComboBox A?
It seems from what I've found on this site that it gets all too complicated all too quickly.
What's the "right" MVVM way to get it done?
Thank you.
EDIT
I updated using Didier's example.
An extract of my XAML:
<ComboBox Name="BrowserStackDesktopOS" ItemsSource="Binding Platforms.AvailableBrowserStackDesktopOSes}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopOSVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopOSVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSVersionSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowser" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowsers}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowserVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowserVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserVersionSelectedValue, Mode=TwoWay}"/>
And an example of my code behind:
public string BrowserStackDesktopOSSelectedValue {
get { return (string)GetValue(BrowserStackDesktopOSSelectedValueProperty); }
set { SetValue(BrowserStackDesktopOSSelectedValueProperty, value);
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
NotifyPropertyChanged("BrowserStackDesktopOSSelectedValue");
}
}
However when I select a value for the first ComboBox nothing happens. I am wanting the Itemsource of the next ComboBox to by populated.
What have I done wrong?
Basically you need to expose in your MVVM 2 collections of values for combo-box choices and two properties for selected values.
In the beginning only the first collection if filled with values. When the first selected value changes the second collection will be filled in with appropriate values. Here is an example implementation:
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//Set the data context of the window
DataContext = new TestVM();
}
}
public class TestVM : INotifyPropertyChanged
{
#region Class attributes
protected static string[] firstComboValues = new string[] { "Choice_1", "Choice_2" };
protected static string[][] secondComboValues =
new string[][] {
new string[] { "value_1_1", "value_1_2", "value_1_3" },
new string[] { "value_2_1", "value_2_2", "value_2_3" }
};
#endregion
#region Public Properties
#region FirstSelectedValue
protected string m_FirstSelectedValue;
/// <summary>
///
/// </summary>
public string FirstSelectedValue
{
get { return m_FirstSelectedValue; }
set
{
if (m_FirstSelectedValue != value)
{
m_FirstSelectedValue = value;
UpdateSecondComboValues();
NotifyPropertyChanged("FirstSelectedValue");
}
}
}
#endregion
#region SecondSelectedValue
protected string m_SecondSelectedValue;
/// <summary>
///
/// </summary>
public string SecondSelectedValue
{
get { return m_SecondSelectedValue; }
set
{
if (m_SecondSelectedValue != value)
{
m_SecondSelectedValue = value;
NotifyPropertyChanged("SecondSelectedValue");
}
}
}
#endregion
#region FirstComboValues
protected ObservableCollection<string> m_FirstComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> FirstComboValues
{
get { return m_FirstComboValues; }
set
{
if (m_FirstComboValues != value)
{
m_FirstComboValues = value;
NotifyPropertyChanged("FirstComboValues");
}
}
}
#endregion
#region SecondComboValues
protected ObservableCollection<string> m_SecondComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> SecondComboValues
{
get { return m_SecondComboValues; }
set
{
if (m_SecondComboValues != value)
{
m_SecondComboValues = value;
NotifyPropertyChanged("SecondComboValues");
}
}
}
#endregion
#endregion
public TestVM()
{
FirstComboValues = new ObservableCollection<string>(firstComboValues);
}
/// <summary>
/// Update the collection of values for the second combo box
/// </summary>
protected void UpdateSecondComboValues()
{
int firstComboChoice;
for (firstComboChoice = 0; firstComboChoice < firstComboValues.Length; firstComboChoice++)
{
if (firstComboValues[firstComboChoice] == FirstSelectedValue)
break;
}
if (firstComboChoice == firstComboValues.Length)// just in case of a bug
SecondComboValues = null;
else
SecondComboValues = new ObservableCollection<string>(secondComboValues[firstComboChoice]);
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And the associated XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="window" x:Class="Testing1.MainWindow">
<Grid>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Width=" 300">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="10"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="FirstOne" ItemsSource="{Binding FirstComboValues}" SelectedItem="{Binding FirstSelectedValue, Mode=TwoWay}"/>
<ComboBox x:Name="SecondOne" ItemsSource="{Binding SecondComboValues}" SelectedItem="{Binding SecondSelectedValue, Mode=TwoWay}" Grid.Row="2"/>
</Grid>
</Grid>
</Window>
As you can see the SelectedValue properties of combo boxes are binded in TwoWay mode so when SelectedValue property of the combo box changes it changes the value on the VM side. And in FirstSelectedValue property setter UpdateSecondComboValues() method is called to update values for the second combo box.
EDIT:
It happens because you mixed both INotifPropertyChanged and DependencyObject. You should choose one of them. Usually you implement INotifyPropertyChanged in your VM and the code in the property setter will work.
If you inherit from DependencyObject however, you should not write any code in the setter/getter. It will never be called by the TwoWay binding. It will just call GetValue(...) internally. To be able to execute an action on DependencyProperty change you should declare it differently with a property changed handler:
#region BrowserStackDesktopOSSelectedValue
/// <summary>
/// BrowserStackDesktopOSSelectedValue Dependency Property
/// </summary>
public static readonly DependencyProperty BrowserStackDesktopOSSelectedValue Property =
DependencyProperty.Register("BrowserStackDesktopOSSelectedValue ", typeof(string), typeof(YourVM),
new FrameworkPropertyMetadata((string)null,
new PropertyChangedCallback(OnBrowserStackDesktopOSSelectedValue Changed)));
/// <summary>
/// Gets or sets the BrowserStackDesktopOSSelectedValue property. This dependency property
/// indicates ....
/// </summary>
public string BrowserStackDesktopOSSelectedValue
{
get { return (string)GetValue(BrowserStackDesktopOSSelectedValue Property); }
set { SetValue(BrowserStackDesktopOSSelectedValue Property, value); }
}
/// <summary>
/// Handles changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
private static void OnBrowserStackDesktopOSSelectedValue Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
YourVM target = (YourVM)d;
string oldBrowserStackDesktopOSSelectedValue = (string)e.OldValue;
string newBrowserStackDesktopOSSelectedValue = target.BrowserStackDesktopOSSelectedValue ;
target.OnBrowserStackDesktopOSSelectedValue Changed(oldBrowserStackDesktopOSSelectedValue , newBrowserStackDesktopOSSelectedValue );
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
protected virtual void OnBrowserStackDesktopOSSelectedValue Changed(string oldBrowserStackDesktopOSSelectedValue , string newBrowserStackDesktopOSSelectedValue )
{
//Here write some code to update your second ComboBox content.
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
}
#endregion
By the way I always use Dr WPF snippets to write DPs so it goes much faster.
Here is my issue, I created a UserControl as follows:
XAML:
<UserControl x:Class="ProcessVisualizationBar.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" xmlns:lb="clr-namespace:ProcessVisualizationBar"
Name="ProcessVisualizationBar">
<Border BorderBrush="Silver" BorderThickness="1,1,1,1" Margin="0,5,5,5" CornerRadius="5" Padding="2">
<ListBox Name="ProcessVisualizationRibbon" Grid.Column="1" Height="40" ItemsSource="{Binding ElementName=ProcessVisualizationBar, Path=ItemsSource}"/>
</Border>
</UserControl>
Code Behind(C#):
using System.Windows;
using System.Windows.Controls;
namespace ProcessVisualizationBar
{
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(System.Collections.IEnumerable), typeof(UserControl));
public System.Collections.IEnumerable ItemsSource
{
get { return ProcessVisualizationRibbon.ItemsSource; }
set { ProcessVisualizationRibbon.ItemsSource = value; }
}
public UserControl1()
{
InitializeComponent();
}
}
}
I build my Usercontrol and add the .dll to the reference of another project. I add the reference at the top of my XAML as such:
xmlns:uc="clr-namespace:ProcessVisualizationBar;assembly=ProcessVisualizationBar"
Then I go to use the control.
<uc:UserControl1 Grid.Row="2" x:Name="ProcessVisualizationContent" />
It finds the control okay, but when I try and find the ItemsSource Property I added to it, I'm not finding it. I'm not sure what I missed, and I'm not sure what debug tools are really available to figure this out.
Anyone have some experience with this that can share their wisdom?
What is the actual data being passed? That is what you should be creating and not a pass through situation which you are attempting.
Create a dependency property targetting the actual data to be passed with a property changed handler. On the change event, then call internal code to bind it to the ProcessVisualazation ItemsSource. That way you can debug when the data comes through by placing a breakpoint in the event.
Here is an example where the consumer will see StringData in the Xaml and needs to pas a list of strings into the custom control:
#region public List<string> StringData
/// <summary>
/// This data is to be bound to the ribbon control
/// </summary>
public List<string> StringData
{
get { return GetValue( StringDataProperty ) as List<string>; }
set { SetValue( StringDataProperty, value ); }
}
/// <summary>
/// Identifies the StringData dependency property.
/// </summary>
public static readonly System.Windows.DependencyProperty StringDataProperty =
System.Windows.DependencyProperty.Register(
"StringData",
typeof( List<string> ),
typeof( UserControl ),
new System.Windows.PropertyMetadata( null, OnStringDataPropertyChanged ) );
/// <summary>
/// StringDataProperty property changed handler.
/// </summary>
/// <param name="d">DASTreeBinder that changed its StringData.</param>
/// <param name="e">Event arguments.</param>
private static void OnStringDataPropertyChanged( System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e )
{
UserControl source = d as UserControl;
List<string> value = e.NewValue as List<string>;
BindDataToRibbon( value );
}
#endregion public List<string> StringData
Now just create a BindDataToRibbon method which will do the dirty work. Note that I use Jeff Wilcox's Silverlight dependency snippets in Visual Studio to generate the above dependency. I have used it for WPF and Silverlight projects.
I am having problems binding to my control. I would like the label(lblLabel) in my control to display the metadata from whatever is bound to the Field Property. It currently displays "Field" as a label. How do I get it to display "Customer Name :" which is the Name on the view model for property, CustomerName?
My Controls XAML
<UserControl x:Name="ctlRowItem" x:Class="ApplicationShell.Controls.RowItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:my="clr-namespace:SilverlightApplicationCore.Controls;assembly=SilverlightApplicationCore"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="g_required" Width="15" />
<ColumnDefinition x:Name="g_label" Width="200" />
<ColumnDefinition x:Name="g_control" Width="auto" />
<ColumnDefinition x:Name="g_fieldEnd" Width="*" />
</Grid.ColumnDefinitions>
<sdk:Label x:Name="lblRequired" Grid.Column="0" Grid.Row="0" />
<sdk:Label x:Name="lblLabel" Grid.Column="1" Grid.Row="3" Target="{Binding ElementName=txtControl}" PropertyPath="Field" />
<TextBox x:Name="txtControl" Grid.Column="2" Grid.Row="3" MaxLength="10" Width="150" Text="{Binding Field, Mode=TwoWay, ElementName=ctlRowItem}" />
</Grid>
</UserControl>
My Controls CODE BEHIND
using System.Windows;<BR>
using System.Windows.Controls;<BR>
using System.Windows.Data;<BR>
using ApplicationShell.Resources;<BR>
namespace ApplicationShell.Controls
{
public partial class RowItem : UserControl
{
#region Properties
public object Field
{
get { return (string)GetValue(FieldProperty); }
set { SetValue(FieldProperty, value); }
}
#region Dependency Properties
public static readonly DependencyProperty FieldProperty = DependencyProperty.Register("Field", typeof(object), typeof(RowItem), new PropertyMetadata(null, Field_PropertyChangedCallback));
#endregion
#endregion
#region Events
#region Dependency Properties
private static void Field_PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != e.NewValue)
return;
var control = (RowItem)d;
control.Field = (object)e.NewValue;
}
#endregion
#endregion
#region Constructor
public RowItem()
{
InitializeComponent();
}
#endregion
}
}
View Model
namespace ApplicationShell.Web.ViewModel
{
[Serializable]
public class Customers
{
[Display(AutoGenerateField = false, ShortName="CustomerName_Short", Name="CustomerName_Long", ResourceType = typeof(LocaleLibrary))]
public override string CustomerName { get; set; }
}
}
XAML which calls the My Control
This pages datacontext is set to a property of type Customers (View Model).
<controls:ChildWindow x:Class="ApplicationShell.CustomerWindow"
xmlns:my="clr-namespace:SilverlightApplicationCore.Controls;assembly=SilverlightApplicationCore"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Title="Customer View">
<my:RowItem x:name="test" Field="{Binding CustomerName,Mode=TwoWay}" />
</controls:ChildWindow>
There is a way of getting at the display names of the properties bound to, but sadly it is not trivial and we have to make assumptions about the property-paths used.
I'm aware that the Silverlight Toolkit ValidationSummary is able to find out property names of bindings automatically, but when I looked through its source code, I found that it does this by doing its own evaluation of the binding path.
So, that's the approach I'll take here.
I modified the code-behind of your RowItem user-control, and this is what I came up with:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
public partial class RowItem : UserControl
{
public RowItem()
{
InitializeComponent();
Dispatcher.BeginInvoke(SetFieldLabel);
}
public string Field
{
get { return (string)GetValue(FieldProperty); }
set { SetValue(FieldProperty, value); }
}
public static readonly DependencyProperty FieldProperty =
DependencyProperty.Register("Field", typeof(string), typeof(RowItem),
null);
/// <summary>
/// Return the display name of the property at the end of the given binding
/// path from the given source object.
/// </summary>
/// <remarks>
/// <para>
/// The display name of the property is the name of the property according
/// to a <see cref="DisplayAttribute"/> set on the property, if such an
/// attribute is found, otherwise the name of the property.
/// </para>
/// <para>
/// This method supports dot-separated binding paths only. Binding
/// expressions such <c>[0]</c> or <c>(...)</c> are not supported and will
/// cause this method to return null.
/// </para>
/// <para>
/// If no suitable property could be found (due to an intermediate value
/// of the property-path evaluating to <c>null</c>, or no property with a
/// given name being found), <c>null</c> is returned. The final property
/// in the path can have a <c>null</c> value, as that value is never used.
/// </para>
/// </remarks>
/// <param name="binding">The binding expression.</param>
/// <param name="source">
/// The source object at which to start the evaluation.
/// </param>
/// <returns>
/// The display name of the property at the end of the binding, or
/// <c>null</c> if this could not be determined.
/// </returns>
private string GetBindingPropertyDisplayName(BindingExpression binding,
object source)
{
if (binding == null)
{
throw new ArgumentNullException("binding");
}
string bindingPath = binding.ParentBinding.Path.Path;
object obj = source;
PropertyInfo propInfo = null;
foreach (string propertyName in bindingPath.Split('.'))
{
if (obj == null)
{
// Null object not at the end of the path.
return null;
}
Type type = obj.GetType();
propInfo = type.GetProperty(propertyName);
if (propInfo == null)
{
// No property with the given name.
return null;
}
obj = propInfo.GetValue(obj, null);
}
DisplayAttribute displayAttr =
propInfo.GetCustomAttributes(typeof(DisplayAttribute), false)
.OfType<DisplayAttribute>()
.FirstOrDefault();
if (displayAttr != null)
{
return displayAttr.GetName();
}
else
{
return propInfo.Name;
}
}
private void SetFieldLabel()
{
BindingExpression binding = this.GetBindingExpression(FieldProperty);
string displayName = GetBindingPropertyDisplayName(binding,
DataContext);
if (lblLabel != null)
{
lblLabel.Content = displayName;
}
}
}
There are a few things to note:
To use this code, your project will need a reference to System.ComponentModel.DataAnnotations. However, that shouldn't be a problem as that's the same reference you need in order to use the Display attribute.
The function SetFieldLabel is called to set the label of the field. I found that the most reliable place to call it was from Dispatcher.BeginInvoke. Calling this method directly from within the constructor or from within a Loaded event handler did not work, as the binding had not been set up by then.
Only binding paths consisting of a dot-separated list of property names are supported. Something like SomeProp.SomeOtherProp.YetAnotherProp are fine, but SomeProp.SomeList[0] is not supported and will not work. If the display name of the binding property cannot be determined, nothing will be displayed.
There's no longer a PropertyChangedCallback on the Field dependency property. We're not really interested in what happens whenever the user changes the text in the control. It's not going to change the display name of the property bound to.
For test purposes, I knocked up the following view-model class:
public class ViewModel
{
// INotifyPropertyChanged implementation omitted.
[Display(Name = "This value is in a Display attribute")]
public string WithDisplay { get; set; }
public string WithoutDisplay { get; set; }
[Display(Name = "ExampleFieldNameKey", ResourceType = typeof(Strings))]
public string Localised { get; set; }
public object This { get { return this; } }
public object TheVerySame { get { return this; } }
}
(The Resources collection Strings.resx contains a single key, with name ExampleFieldNameKey and value This value is in a Resources.resx. This collection also has its Access Modifier set to Public.) I tested out my modifications to your control using the following XAML, with the DataContext set to an instance of the view-model class presented above:
<StackPanel>
<local:RowItem Field="{Binding Path=WithDisplay, Mode=TwoWay}" />
<local:RowItem Field="{Binding Path=WithoutDisplay, Mode=TwoWay}" />
<local:RowItem Field="{Binding Path=Localised, Mode=TwoWay}" />
<local:RowItem Field="{Binding Path=This.This.TheVerySame.This.WithDisplay, Mode=TwoWay}" />
</StackPanel>
This gave me four RowItems, with the following labels:
This value is in a Display attribute
WithoutDisplay
This value is in a Resources.resx
This value is in a Display attribute
I have a problem with DataContextChanged not being raised on a logical child of my custom Panel control. I've narrowed it down to this:
Starting from a wizard-generated WPF application I add:
private void Window_Loaded( object sender, RoutedEventArgs e )
{
var elt = new FrameworkElement();
this.AddLogicalChild( elt );
DataContext = 42;
Debug.Assert( (int)elt.DataContext == 42 );
}
As I understand, this works because the DataContext is an inheritable dependency property.
Now, I add event handlers for DataContextChanged both on the Window (this) and its logical child:
this.DataContextChanged +=
delegate { Debug.WriteLine( "this:DataContextChanged" ); };
elt.DataContextChanged +=
delegate { Debug.WriteLine( "elt:DataContextChanged" ); };
If I run this, only the first event handler will execute. Why is this? If instead of AddLogicalChild( elt ) I do the following:
this.Content = elt;
both handlers will execute. But this is not an option in my case - I'm adding FrameworkContentElements to my control which aren't supposed to be visual children.
What's going on here? Should I do something more besides AddLogicalChild() to make it work?
(Fortunately, there is a rather simple workaround - just bind the DataContext of the element to the DataContext of the window)
BindingOperations.SetBinding( elt, FrameworkElement.DataContextProperty,
new Binding( "DataContext" ) { Source = this } );
Thank you.
You need to override the LogicalChildren property too:
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return elt; }
}
Of course, you'll want to return any logical children defined by the base implementation, too.
I'd like to add some advice to Kent's answer if someone runs into a similar problems:
If you create a custom control with multiple content objects you should ensure:
Content objects are added to the LogicalTree via AddLogicalChild()
Create your own Enumerator class and return an instance of that in the overriden LogicalChildren property
If you don't add the content objects to the logical tree you might run into problems like Bindings with ElementNames can not be resolved (ElementName is resolved by FindName which in turn uses the LogicalTree to find the elements).
What makes it even more dangerous is that my experience is that if you miss to add the objects to the logical tree the ElementName resolving works in some scenarios and it doesn't work in other scenarios.
If you don't override LogicalChildren the DataContext is not updated like described above.
Here a short example with a simple SplitContainer:
SplitContainer:
public class SplitContainer : Control
{
static SplitContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitContainer), new FrameworkPropertyMetadata(typeof(SplitContainer)));
}
/// <summary>
/// Identifies the <see cref="Child1"/> property.
/// </summary>
public static readonly DependencyProperty Child1Property =
DependencyProperty.Register(nameof(Child1), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child1PropertyChangedCallback));
/// <summary>
/// Left Container
/// </summary>
public object Child1
{
get { return (object)GetValue(Child1Property); }
set { SetValue(Child1Property, value); }
}
/// <summary>
/// Identifies the <see cref="Child2"/> property.
/// </summary>
public static readonly DependencyProperty Child2Property =
DependencyProperty.Register(nameof(Child2), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child2PropertyChangedCallback));
/// <summary>
/// Right Container
/// </summary>
public object Child2
{
get { return (object)GetValue(Child2Property); }
set { SetValue(Child2Property, value); }
}
private static void Child1PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
private static void Child2PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
protected override IEnumerator LogicalChildren
{
get
{
return new SplitContainerLogicalChildrenEnumerator(this);
}
}
}
SplitContainerLogicalChildrenEnumerator:
internal class SplitContainerLogicalChildrenEnumerator : IEnumerator
{
private readonly SplitContainer splitContainer;
private int index = -1;
public SplitContainerLogicalChildrenEnumerator(SplitContainer splitContainer)
{
this.splitContainer = splitContainer;
}
public object Current
{
get
{
if (index == 0)
{
return splitContainer.Child1;
}
else if (index == 1)
{
return splitContainer.Child2;
}
throw new InvalidOperationException("No child for this index available");
}
}
public bool MoveNext()
{
index++;
return index < 2;
}
public void Reset()
{
index = -1;
}
}
Style (e.g. in Themes/generic.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=PresentationFramework"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SplitContainerElementNameProblem">
<Style TargetType="{x:Type local:SplitContainer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SplitContainer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Child1}" />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Child2}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Sample which demonstrates that each Binding works fine:
XAML:
<Window x:Class="SplitContainerElementNameProblem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SplitContainerElementNameProblem"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBox x:Name="text1" Text="abc" />
</Grid>
<local:SplitContainer Grid.Row="1">
<local:SplitContainer.Child1>
<TextBox x:Name="text2"
Text="{Binding ElementName=text1, Path=Text}" />
</local:SplitContainer.Child1>
<local:SplitContainer.Child2>
<StackPanel>
<TextBox x:Name="text3"
Text="{Binding ElementName=text2, Path=Text}" />
<TextBox x:Name="text4"
Text="{Binding MyName}" />
</StackPanel>
</local:SplitContainer.Child2>
</local:SplitContainer>
</Grid>
XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyName = "Bruno";
}
public string MyName
{
get;
set;
}
}