Why does data binding on a dynamically loaded control break? - wpf

Currently I'm designing an application that should at one point be able to create a report from a Xaml template File using Data Binding (involving a FlowDocument).
The idea was to simply convert a dynamically loaded control via BlockUIContainer to be printable in a FlowDocument.
As long as I load an entire file into a single FrameworkElement and set the DataContext property, the Data Binding works like a charm.
foreach (Order order in orders)
{
BlockUIContainer container = new BlockUIContainer();
container.Child = (FrameworkElement)GetOrderControl();
(container.Child as FrameworkElement).DataContext = order;
document.Blocks.Add(container);
}
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.SystemIdle,
new Action(() => { return; }));
All the GetOrderControl() method does is read from a FileStream a parse the content via XamlReader.Load(). The file is structured like this:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
...
<TextBlock Text="{Binding Path=Country}" />
...
</Gird>
Now the application should add BlockUIContainers dynamically according to the dataset. I need to do it in code behind to implement custom pagination, because the reports might get longer than one page.
Since I only want a single template file, I've packed my header, footer and grouping controls all in a single xaml file like this:
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BlockUIContainer Name="PageHeader">
<Grid ... />
</BlockUIContainer>
<BlockUIContainer Name="Element">
<Grid ... />
</BlockUIContainer>
</FlowDocument>
The <Grid ... /> control inside the "Element" named BlockUIContainer is just exactly the Grid control used in the example before.
Now all I do is get the child of the BlockUIContainer and create a copy of that by saving it to a string and back to a FrameworkElement and set the DataContext.
foreach (Order order in orders)
{
BlockUIContainer container = new BlockUIContainer();
container.Child = (FrameworkElement)XamlReader.Parse(XamlWriter.Save(elementControl));
(container.Child as FrameworkElement).DataContext = order;
document.Blocks.Add(container);
}
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.SystemIdle,
new Action(() => { return; }));
Here however the data binding is not evaluating. I tried calling the UpdateLayout() method on the FrameworkElement after setting the DataContext.
That does evaluate at least some Bindings in a <DataTrigger Binding="{Binding Path=DangerousGoods}" /> for a <Style> Element setting the Visibility of some child controls, but none of the Bindings like <TextBlock Text="{Binding Path=Country}" /> are not evaluated.
I'm at a loss here. How do I get the remaining bindings to work again after parsing them? I don't really want to create several files for one document.

Nevermind, I found the error... the Bindings get evaluated the first time the control is created.
The XamlWriter then "destroys" the Binding by evaluating the text and writing the original text output (which was empty) into the element's Text property.

Related

How to have dynamically loaded instances of unbound named XAML elements behave independently from each other?

I have a WPF application where I dynamically load document view instances into a TabControl. The view has a ToolBar with some ToggleButtons which I use to control the visibility of certain elements in that view like so (only relevant elements shown):
<UserControl x:Class="MyProject.View.Views.DocumentView" ...>
...
<ToolBar>
<ToggleButton x:Name="togglePropInspector" ... />
...
</ToolBar>
...
<Border Visibility={Binding ElementName=togglePropInspector, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
...
</Border>
</UserControl>
I found this kinda neat as everything is handled inside the view and didn't require adding code to the view model (or code behind). However, the problem is that checking the toggle button on one tab now checks it in all instances of the view, not just the current tab. This basically applies to all elements whose state is not bound to the view model in any way. Is there a way around this without having to add code to the view model?
For completeness' sake here's the relevant part of how I'm loading the views:
<TabControl ItemsSource="{Binding Documents}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DocumentViewModel}">
<local:DocumentView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
TabControl has a single content host that is used for all TabItem instances. When the data models assigned to the TabItem.Content property are of the same data type, then the TabControl will reuse the same DataTemplate, which means same element instances, only updated with the changed data from data bindings.
To change the state of the reused controls, you must either access the control explicitly, or force the TabControl to reapply the ContentTemplate by temporarily changing the data type of the Content:
<TabControl SelectionChanged="TabControl_SelectionChanged" />
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var tabControl = sender as TabControl;
var tabItemContainer = tabControl.ItemContainerGenerator.ContainerFromItem(tabControl.SelectedItem) as TabItem;
object currentContent = tabItemContainer.Content;
tabItemContainer.Content = null;
// Defer and leave the context to allow the TabControl to handle the new data type (null).
// The content switch shouldn't be noticable in the GUI.
Dispatcher.InvokeAsync(() => tabItemContainer.Content = currentContent);
}
You can also use a dedicated data type for each tab. This way the TabControl is automatically forced to switch the DataTemplate.
The cleanest solution would be to bind the ToggleButton to the data model.

XAML Binding only works on first item of array?

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}...

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;

TabControl disposes of controls on inactive tabs

I'm using the MVVM pattern for my app. The MainWindow comprises a TabControl with the DataContext mapped to the ViewModel:
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="templateMainTabControl">
<ContentPresenter Content="{Binding Path=DisplayName}" />
</DataTemplate>
<local:ViewModel x:Key="VM" />
<local:WorkspaceSelector x:Key="WorkspaceSelector" />
<local:TabOneView x:Key="TabOneView" />
<local:TabTableView x:Key="TabTableView" />
<DataTemplate x:Key="TabOne">
<local:TabOneView />
</DataTemplate>
<DataTemplate x:Key="TabTable">
<local:TabTableView />
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<TabControl Grid.Row="0"
DataContext="{StaticResource VM}"
ItemsSource="{Binding Workspaces}"
SelectedItem="{Binding SelectedWorkspace}"
ItemTemplate="{StaticResource templateMainTabControl}"
ContentTemplateSelector="{StaticResource WorkspaceSelector}" />
The WorkspaceSelector looks like:
public class WorkspaceSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate( object item, DependencyObject container )
{
Window win = Application.Current.MainWindow;
Workspace w = ( Workspace ) item;
string key = w.DisplayName.Replace( " ", "" );
if ( key != "TabOne" )
{
key = "TabTable";
}
return win.FindResource( key ) as DataTemplate;
}
}
so that TabOne returns the DataTemplate. TabOne and the other two tabs return the DataTemplate TabTable.
If I run the application and click on each of the tabs twice (1, 2, 3, 1, 2, 3) I don't get what I expect, which is
TabOne's view is created
TabTwo's view is created
TabOne's View is created
TabTwo's view is created
that is, if the TemplateSelector returns a different value, the existing tab's controls are thrown away and the new tab's control's are created, and if the TemplateSelector returns the same value, nothing happens.
This is exactly what I don't want! I'd like the TabControl to keep all the controls on the tabs, and I would like to be able to do something about creating different controls in code for the case where I go from TabTwo to TabThree. I can live without the latter. But how do I tell the TabControl not to throw away each tab's controls when it's not selected?
This is a function of the TabControl and is the default behavior.
Basically, to save memory, the TabControl unloads the visual tree that is in its content area and replaces it with a newly crufted up one for the new tab. To prove this to yourself, you can listen to the Unload event on each control you template in and notice that it fires every time you switch tabs.
There are likely 2 reasons you would want to override this behavior:
You believe that there would be a significant performance penalty.
You are losing the state of the controls because any visual state that is being lost is not backed by a ViewModel.
As for #1, you shouldn't be concerned. CPU time is generally cheaper than RAM and the default behavior leans on the cheaper side of the resource equation. If you still feel like you REALLY don't want this behavior, you can see an example of overriding it here:
https://github.com/cefsharp/CefSharp/blob/master/CefSharp.Wpf.Example/Controls/NonReloadingTabControl.cs
However, I would consider this a "smell" for potentially a future performance issue you should spend the time figuring out now, rather than delaying figuring it out.
For #2, you have two options:
Make sure every property you want preserved (like IsSelected, etc) is backed by a ViewModel that preserves that state.
Create a persistent UserControl for each tab that you bind to, rather than to ViewModels (Workspaces in your case). There is an example of that in the "Writer" sample for WAF: http://waf.codeplex.com/

Resources