XAML Binding only works on first item of array? - wpf

I have a custom user control which has a dependency property as follows:
public static readonly DependencyProperty PagesProperty =
DependencyProperty.Register("Pages", typeof(IEnumerable<MyContentPage>), typeof(UC_ApplicationWindow),
new PropertyMetadata(new List<MyContentPage>()));
public IList<MyContentPage> Pages
{
get => (IList<MyContentPage>)GetValue(PagesProperty);
set => SetValue(PagesProperty, value);
}
I use this in another project something like this:
<graphicElements:UC_ApplicationWindow>
<graphicElements:UC_ApplicationWindow.Pages>
<x:Array Type="{x:Type graphicElements:MyContentPage}">
<graphicElements:MyContentPage>
<graphicElements:MyContentPage.Content>
<StackPanel>
<ContentControl DataContext="{Binding DataContext, ElementName=gd_Main}">
...some content
</ContentControl>
...other content
</StackPanel>
</graphicElements:MyContentPage.Content>
</graphicElements:MyContentPage>
<graphicElements:MyContentPage>
<graphicElements:MyContentPage.Content>
<StackPanel>
<ContentControl DataContext="{Binding DataContext, ElementName=gd_Main}">
...some content
</ContentControl>
...other content
</StackPanel>
</graphicElements:MyContentPage.Content>
</graphicElements:MyContentPage>
</x:Array>
</graphicElements:UC_ApplicationWindow.Pages>
</graphicElements:UC_ApplicationWindow>
Basically in one part of the content I'm trying to pull the DataContext from the context of the parent grid (gd_Main) instead of the page that is passed down to it. My ElementName binding works... for the first element in the array. For all other elements in the array I get this:
Cannot find source for binding with reference 'ElementName=gd_Main'. BindingExpression:Path=DataContext; DataItem=null; target element is 'ContentControl' (Name=''); target property is 'DataContext' (type 'Object')
What am I missing? Why would it bind properly for the first item and not for the rest? Is there a better way to go about this?

Ok I'm not sure if this is the best solution but I got it to work. What I ended up doing was basically going brute force and manually setting the datacontext. I created the value in the constructor as an observable collection and subscribed to the collection changed event like this:
Pages = new ObservableCollection<MyContentPage>();
((ObservableCollection<MyContentPage>)Pages).CollectionChanged += UC_ApplicationWindow_CollectionChanged;
Then in the collection changed event I bind the data context of the individual items to the data context of the current control. Here is that method:
void UC_ApplicationWindow_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (MyContentPage pg in e.NewItems.OfType<MyContentPage>())
{
var bnd = new Binding(nameof(DataContext)) {Source = this};
pg.Content.SetBinding(FrameworkElement.DataContextProperty, bnd);
}
}
Note that I couldn't just directly set the DataContext because on initialization while it's parsing out the XAML the data context is not yet set, plus it may change later, so I needed to create an actual binding so it updates properly.
Also worth noting. This is essentially doing the same thing as the comment by #Shivani Katukota above with one difference which is fairly crucial in my case but may be inconsequential in others. This method binds in code behind as items are added to the collection, binding in XAML means that you have to do it in the XAML for each item. My control is designed to have other users use it in other projects so having to set it in XAML would require me to give the user that instruction and it wouldn't work right if they didn't do so. If you are doing this internally it's not a really big deal to just use {x:Reference}...

Related

Cannot set DataContext of custom UserControl declaratively

I have created a small UserControl named EventMenu. I reference it in a larger view using:
<views:EventMenu
Grid.Row="1"
DataContext="{Binding DataContext.ServicingEventsMenuViewModel}"
/>
(I've also tried DataContext="{Binding Path=DataContext.ServicingEventsMenuViewModel}").
However, no content from the bound data context appears in the control (static content from the control's XAML file does appear). I believe the control is not binding the DataContext.
In the code behind constructor for the UserControl, I've done this:
public EventMenu()
{
InitializeComponent();
if (DataContext == null)
{
Console.WriteLine("No data context!");
}
}
and it does indicate that the DataContext is null.
What do I need to do to ensure that the DataContext is set on my UserControl?
The first wrong thing i see is this :
DataContext="{Binding DataContext.ServicingEventsMenuViewModel}"
DataContext is a Dependency Property of FrameworkElement.
So when writing something like you'v written , you wrote it as if your DataContext had a property called DataContext , it's the equivalent of writing this :
DataContext="{Binding Path=DataContext.ServicingEventsMenuViewModel}"
Are you trying to reach a parent element's DataContext ?
if so you need to write something like this ( for example if it's parent is a UserControl):
DataContext="{Binding Path=DataContext.ServicingEventsMenuViewModel,RelativeSource={RelativeSource AncestorType={x:Type UserControl}"
I'm guessing that's the case here ...
let me ask you this , post the xaml in which
<views:EventMenu />
is nested and the class holding ServicingEventsMenuViewModel property .
The second thing i think is wrong is to checking if your DataContext is null in the constructor , even if you did write the correct binding statement this property would still be null in the constructor.
subscribe to the FrameworkElement's Loaded event and check it there.

Best practices for WPF MVVM UserControls. Avoiding Binding problems

I am trying to be a good soldier and design some simple User Controls for use in WPF MVVM applications. I am trying (as much as possible) to make the UserControls themselves use MVVM, but I don't think the calling app should know that. The calling app should just be able to slap down the user control, perhaps set one or two properties, and perhaps subscribe to events. Just like when they use a regular control (ComboBox, TextBox, etc.) I'm having a heck of a time getting the bindings right. Notice the use of ElementName in the below View. This is instead of using DataContext. Without further ado, here is my control:
<UserControl x:Class="ControlsLibrary.RecordingListControl"
...
x:Name="parent"
d:DesignHeight="300" d:DesignWidth="300">
<Grid >
<StackPanel Name="LayoutRoot">
<ListBox ItemsSource="{Binding ElementName=parent,Path=Recordings}" Height="100" Margin="5" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=FullDirectoryName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
In the code behind (if there is a way to avoid code behind, please tell me)...
public partial class RecordingListControl : UserControl
{
private RecordingListViewModel vm = new RecordingListViewModel();
public RecordingListControl()
{
InitializeComponent();
// I have tried the next two lines at various times....
// LayoutRoot.DataContext = vm;
//DataContext = vm;
}
public static FrameworkPropertyMetadata md = new FrameworkPropertyMetadata(new PropertyChangedCallback(OnPatientId));
// Dependency property for PatientId
public static readonly DependencyProperty PatientIdProperty =
DependencyProperty.Register("PatientId", typeof(string), typeof(RecordingListControl), md);
public string PatientId
{
get { return (string)GetValue(PatientIdProperty); }
set { SetValue(PatientIdProperty, value);
//vm.SetPatientId(value);
}
}
// this appear to allow us to see if the dependency property is called.
private static void OnPatientId(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecordingListControl ctrl = (RecordingListControl)d;
string temp = ctrl.PatientId;
}
In my ViewModel I have:
public class RecordingListViewModel : ViewModelBase
{
private ObservableCollection<RecordingInfo> _recordings = null;// = new ObservableCollection<string>();
public RecordingListViewModel()
{
}
public ObservableCollection<RecordingInfo> Recordings
{
get
{
return _recordings;
}
}
public void SetPatientId(string patientId)
{
// bunch of stuff to fill in _recordings....
OnPropertyChanged("Recordings");
}
}
I then put this control down in my main window and like so:
<Grid>
<ctrlLib:RecordingListControl PatientId="{Binding PatientIdMain}" SessionId="{Binding SessionIdMain}" />
<Label Content="{Binding PatientIdMain}" /> // just to show binding is working for non-controls
</Grid>
The error I get when I run all this is:
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''RecordingListControl' (Name='parent')'. BindingExpression:Path=Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
Clearly I have some sort of bindings problem. This is actually much further than I was getting. At least I'm hitting the code in the controls code behind:
OnPatientId.
Before, I didn't have the ElementName in the User Control and was using DataContext and was getting a binding error indicating that PatientIdMain was being considered a member of the user control.
Can someone point me to an example of using a User Control with MVVM design in a MVVM application? I would think this is a fairly common pattern.
Let me know if I can provide more details.
Many thanks,
Dave
Edit 1
I tried har07's idea (see one of the answers). I got:
If I try:
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
I get
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=DataContext.Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
If I try:
ItemsSource="{Binding Recordings}"
I get
System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=Recordings; DataItem='MainViewModel' (HashCode=59109011); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
I think his first idea (and maybe his second) are very close, but recall, Recordings is defined in the ViewModel, not the view. somehow I need to tell XAML to use viewModel as source. That's what setting the DataContext does, but as I said in the main part, that creates problems elsewhere (you get binding errors related to binding from the MainWindown to properties on the control).
Edit 2. If I try har07's first suggestion:
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
AND add in the code behind for the control:
RecordingListViewModel vm = new RecordingListViewModel();
DataContext = vm;
I get:
System.Windows.Data Error: 40 : BindingExpression path error: 'PatientIdMain' property not found on 'object' ''RecordingListViewModel' (HashCode=33515363)'. BindingExpression:Path=PatientIdMain; DataItem='RecordingListViewModel' (HashCode=33515363); target element is 'RecordingListControl' (Name='parent'); target property is 'PatientId' (type 'String')
in other words, the control seems fine, but the binding of the dependency proprerties to the main window seem messed up. The compiler assumes that PatientIdMain is part of RecordingListViewModel.
Various posts indicated that I couldn't set DataContext for this very reason. It would mess up bindings to the main window. See for example:
Binding to a dependency property of a user control WPF/XAML and check out Marc's answer.
You should not set x:Name in a UserControl, since the control has only one Name property, and normally that would be set through the code which uses the control. So, you can't use an ElementName Binding in order to bind to properties of the UserControl itself. Another way to bind to properties of the UserControl inside its content would be to use
{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type my:RecordingsControl}}, Path=Recordings}
In the same way, the client code which uses the control sets its DataContext, either explicitly or through inheritance. Therefore, the instance of the vm you create in the constructor is discarded and replaced by the inherited DataContext as soon as the control is displayed.
You can do two things to solve this: either create the vm outside the UserControl, e.g. as a property of the main window's ViewModel, and use the UserControl like this:
<my:RecordingsControl DataContext="{Binding RecordingListVM}"/>
This way, you wouldn't need any code behind, and the Binding above would simply change to
{Binding Recordings}
Or, create a Recordings property in the UserControl's code behind file, and bind to it as I showed in the first code example.
What you get if binding statement changed this way :
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"
or this way :
ItemsSource="{Binding Recordings}"
If one of above binding way solve current binding error ("BindingExpression path error: 'Recordings' property not found..."), but lead to another binding error please post the latter error message.
I think the correct binding statement for this part is as mentioned above.
UPDATE :
Responding to your edit. Try to set DataContext locally at StackPanel level, so you can have UserControl set to different DataContext :
public RecordingListControl()
{
InitializeComponent();
RecordingListViewModel vm = new RecordingListViewModel();
LayoutRoot.DataContext = vm;
}
again I can see you've tried this but I think this is the correct way to solve particular binding error ("BindingExpression path error: 'PatientIdMain' property not found..."), so let me know if this solve the error but lead to another binding error.
One possible answer is this (inspired by Dependency property binding usercontrol with a viewmodel)
Simply use DataContext for the UserControl and don't use any of the ElementName business.
I.e.
public partial class RecordingListControl : UserControl
{
public RecordingListViewModel vm = new RecordingListViewModel();
public RecordingListControl()
{
InitializeComponent();
**DataContext = vm;**
}
....
Then, in the MainWindow, you need to bind the user control like so:
<ctrlLib:RecordingListControl
Name="_ctrlRecordings"
PatientId="{Binding RelativeSource={RelativeSource
AncestorType=Window},Path=DataContext.PatientIdMain, Mode=TwoWay}"
SessionId="{Binding RelativeSource={RelativeSource
AncestorType=Window},Path=DataContext.SessionIdMain, Mode=TwoWay}" />
I won't mark this as answer because (besides the audacity of answer one's own question) I don't really like the answer. It forces the application programmer, to remember to put in all this nonsense about AncestorType and RelativeSource. I don't think one has to do this with standard controls, so why here?

Changing a datatemplate at runtime

I need to wrap a datatemplate in a datatemplate that gets built at run time. The wrapped datatemplate is WPF element where as the wrapping template needs to be created in code.
Something like:
public DataTemplate GetTemplate(DataTemplate template)
{
string xaml = string.Format(#"
<DataTemplate>
<ContentControl Content=""{{Binding}}"">
<ContentControl.ContentTemplate>
{0}
</ContentControl.ContentTemplate>
</ContentControl>
</DataTemplate>", template);
return CreateTemplate(xaml);
}
Obviously my datatemplate is more complicated then the one I'm using above.
I dont know of anyway to take an existing xaml element and convert it to a string. It seems like I might be able to use FrameworkElementFactory but I see it is depricated, which leads me to think I'm missing something obvious.
EDITED ---
What I'm doing is creating a control that users will supply a datatemplate but I need to make changes to the the template. Maybe this example will make more sense...
public DataTemplate GetTemplate2()
{
// this template would be supplied by the user
// I'm creating it here as an example
string t = string.Format(#"
<DataTemplate>
<TextBlock Text=""{{Binding Value}}""/>
</DataTemplate>");
T = CreateTemplate(t);
string xaml = string.Format(#"
<DataTemplate>
<ContentControl Content=""{{Binding}}"">
<ContentControl.ContentTemplate>
{0}
</ContentControl.ContentTemplate>
</ContentControl>
</DataTemplate>", t);
return CreateTemplate(xaml);
}
This all works because I'm using the string template (e.g. t). However I need to figure out some way to do it with the actual DataTemplate (e.g. T). Unfortunately XamlWriter can't deal with the Binding.
You can create a DataTemplate selector. There you can add your logic to build your DataTemplate at runtime. Also you can create a dependencyProperty in your DataTemplate selector. Then bind it in your xaml to a DataTemplate stored in some backing model, and there do what ever ...
This link might be a good place to start
You can use XamlWriter (the analog to XamlReader) but it has limitations on what can be properly serialized. Things like event handlers and x:Names cause issues.
**UPDATE
Seeing the additional detail I think you should try reversing your approach. Rather than combining the templates using strings and then trying to turn that into the object you want, you can avoid all the weird parsing restrictions by just creating the user's template as a DataTemplate object and then building your own DataTemplate object around it. Your example code is also using 2 Value Paths, which is going to give you .Value.Value on the inner template Text so check to make sure on your real one that you're ending up with the Paths you want. Here's the basics of your example using the objects instead, with the paths updated to expect a String and display its length:
DataTemplate T = XamlReader.Parse(string.Format(#"
<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<TextBlock Text=""{{Binding}}""/>
</DataTemplate>")) as DataTemplate;
FrameworkElementFactory controlFactory = new FrameworkElementFactory(typeof(ContentControl));
controlFactory.SetBinding(ContentControl.ContentProperty, new Binding("Length"));
controlFactory.SetValue(ContentControl.ContentTemplateProperty, T);
DataTemplate mainTemplate = new DataTemplate { VisualTree = controlFactory };

Binding inside ContentControl not working

I'm building a graphical designer, based upon an article by Sukram in CodeProject. I'm now trying to extend it so that each item on the canvas binds to a different ViewModel object - i.e. I'm setting the DataContext for each item.
Every item on the designer is actually a ContentControl, into which is placed a different template (based upon which toolbox item was dragged onto the canvas). So I have a template containing a TextBox, and I have a ViewModel object containing a Name property, and I bind the Text property of the TextBox to the Name property of the ViewModel, and ... nothing. I've checked the visual tree with Snoop, and it confirms that the DataContext of the TextBox is the ViewModel object. Yet the TextBox remains empty. And if I modify the (empty) Text in the TextBox, the Name property in the ViewModel does not change. So it looks like the binding is not being applied (or has been removed somehow).
I've found a few posts which talk about the ContentControl messing around with the DataContext and Content properties, but I'm not sure how applicable they all are. The code sets the ContentControl.Content as follows:
newItem = new ContentControl();
ControlTemplate template = toolbox.GetTemplate();
UIElement element = template.LoadContent() as UIElement;
ViewModelItem viewModel = new ViewModelItem() { Name = "Bob" };
newItem.Content = element;
newItem.DataContext = viewModel;
and the XAML for the template is:
<ControlTemplate>
<Border BorderBrush="Black" BorderThickness="1" Width="100">
<TextBox Text={Binding Name}/>
</Border>
</ControlTemplate>
Snoop shows that the TextBox has a DataContext, and if I Delve that DataContext I can see that it has a Name property whose value is "Bob". So why does the TextBox remain empty? Snoop allows me to change that Name property, but the TextBox remains empty.
What am I doing wrong?
A few more details. I've set the VS2010 Debug DataBinding option for the OutputWindow to Verbose, which seems to show that the binding is all being attempted before I set the DataContext. Is it possible that the change to the DataContext is not being recognised?
I've just found this post DataTemplate.LoadContent does not preserve bindings - apparently DataTemplate.LoadContent does not preserve bindings. So it looks like I have to write my own version of LoadContent().
I've realised that the template has come through a XamlWriter, which apparently strips all bindings. This wouldn't be helping.
I've not been able to fix the DataTemplate.LoadContent(), but I realised that I didn't actually need a DataTemplate, since the XamlWriter / XamlReader was already instantiating the UI element that I was after. I found a fix to make the XamlWriter write all the bindings here, and after that it all works.
Thanks for your help.
Maybe you need to tell the binding in the ControlTemplate to look at the TemplatedParent, as is mentioned in this thread?
<TextBox Text="{Binding Path=Name, RelativeSource={RelativeSource TemplatedParent}}"/>
Either that, or try to use a DataTemplate instead.
I can't test this at the moment, so I might just be guessing here.
I would use a DataTemplate, as bde suggests.
You are trying to put some UI on your own data (ViewModel), and this is what Data-Templates are meant for (ControlTemplate is usually what you use if you want to change how e.g. a Button looks).
Change your code to use ContentControl.ContentTemplate with a DataTemplate:
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Width="100">
<TextBox Text={Binding Name}/>
</Border>
</DataTemplate>
Code-behind:
newItem = new ContentControl();
//NOTE: .GetTemplate() needs to return a DataTemplate, and not a ControlTemplate:
newItem.ContentTemplate = toolbox.GetTemplate();
ViewModelItem viewModel = new ViewModelItem() { Name = "Bob" };
newItem.Content = viewModel;
newItem.DataContext = viewModel;

How to do simple Binding in Silverlight?

I understand that Silverlight 3.0 has binding but just want a simple example on how to use this to read a property from a class.
I have a class called Appointment which as a String property called Location:
Public Property Location() As String
Get
Return _Location
End Get
Set(ByVal Value As String)
_Location = Value
End Set
End Property
With a Private Declaration for the _Location as String of course.
I want a XAML element to bind to this property to display this in a TextElement, but it must be in XAML and not code, for example I want something like this:
<TextBlock Text="{Binding Appointment.Location}"/>
What do I need to do to get this to work?
It has to be a Silverlight 3.0 solution as some WPF features are not present such as DynamicResource which is what I'm used to using.
Just to add that my XAML is being loaded in from a seperate XAML File, this may be a factor in why the binding examples don't seem to work, as there are different XAML files the same Appointment.Location data needs to be applied.
You have two options.
If the "Appointment" class can be used as the DataContext for the control or Window, you can do:
<TextBlock Text="{Binding Location}" />
If, however, "Appointment" is a property of your current DataContext, you need a more complex path for the binding:
<TextBlock Text="{Binding Path=Appointment.Location}" />
Full details are documented in MSDN under the Binding Declarations page. If neither of these are working, make sure you have the DataContext set correctly.
You need something in code, unless you want to declare an instance of Appointment in a resource and bind to that but I doubt thats what you want.
You need to bind the Text property to the Property Path "Location" then assign the DataContext of the containing XAML to an instance of the Appointment:-
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="{Binding Location}" />
</Grid>
Then in the control's load event:-
void Page_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Appointment() { Location = "SomePlace" };
}
Note in this case I'm using the default Page control.
If I'm reading correctly, you need to create an instance of Appointment, set the DataContext of the control to that instance and modify your binding to just say: Text="{Binding Location}"
Also, consider implementing INotifyPropertyChanged on your Appointment class to allow the data classes to notify the UI of property value changes.

Resources