Restoring Bindings after XamlReader parsing - wpf

my problem is the following: In my program I let the user place shapes (class DrawingShape) on a Canvas. The Drawing Shape encapsulates a stacked path and label:
<UserControl x:Class="HandballTrainerFluent.Graphics.DrawingShape"
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"
d:DesignHeight="60"
d:DesignWidth="60"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid x:Name="container" Width="Auto" Height="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="38"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Canvas x:Name="geometryCanvas" HorizontalAlignment="Center" Grid.Row="0" Width="38" Height="38">
<Path x:Name="Path"
Width="35.8774"
Height="31.2047"
Canvas.Left="1.0613"
Canvas.Top="3.29528"
Stretch="Fill"
StrokeLineJoin="Round"
Stroke="{Binding OutlineBrush,Mode=OneWay}"
StrokeThickness="{Binding OutlineWidth,Mode=OneWay}"
StrokeDashArray="{Binding OutlinePattern,Mode=OneWay}"
Fill="{Binding FillBrush,Mode=OneWay}"
Data="F1 M 19,3.79528L 1.5613,34L 36.4387,34L 19,3.79528 Z "/>
</Canvas>
<TextBlock x:Name="TextBox" HorizontalAlignment="Center" Grid.Row="1" Text="{Binding LabelText,Mode=OneWay}"></TextBlock>
</Grid>
</UserControl>
So some visual setting and the label text are bound to Properties of the code-behind file.
After deserializing a Canvas with these Drawing shapes, I need to restore the binding between the XAML and the code-behind file. I've tried this, but it does not seem to work:
private void RepairBindingsAfterLoading()
{
foreach (UIElement element in this.drawingCanvas.Children)
{
if (element.GetType() == typeof(DrawingShape))
{
DrawingShape shape = (DrawingShape)element;
BindingOperations.ClearAllBindings(shape.Path);
BindingOperations.ClearAllBindings(shape.TextBox);
BindingOperations.ClearAllBindings(shape);
shape.BeginInit();
Binding dataContextBinding = new Binding();
dataContextBinding.RelativeSource = RelativeSource.Self;
shape.SetBinding(DrawingShape.DataContextProperty, dataContextBinding);
Binding fillBinding = new Binding("FillBrush");
shape.Path.SetBinding(Path.FillProperty, fillBinding);
Binding outlineBinding = new Binding("OutlineBrush");
shape.Path.SetBinding(Path.StrokeProperty, outlineBinding);
Binding widthBinding = new Binding("OutlineWidth");
shape.Path.SetBinding(Path.StrokeThicknessProperty, widthBinding);
Binding patternBinding = new Binding("OutlinePattern");
shape.Path.SetBinding(Path.StrokeDashArrayProperty, patternBinding);
Binding labelTextBinding = new Binding("LabelText");
shape.TextBox.SetBinding(TextBlock.TextProperty, labelTextBinding);
shape.EndInit();
shape.UpdateLayout();
}
}
}
No matter what I do to the code-behind Properties (e.g. change FillBrush), the visuals of the displayed DrawingShape won't update. Am I missing an important step here?
I've added shape.BeginUpdate() and shape.EndUpdate() after seeing this question: Bindings not applied to dynamically-loaded xaml
Thanks a lot for any insights
Edit 2012-09-25
Looking at another piece of code which does not depend on any bindings makes me wonder, if I can actually reference any elements from the Xaml-Definition via their x:Name after de-serialization. The following callback does not do anything on a shape:
private void rotateClockwiseMenuItem_Click(object sender, RoutedEventArgs e)
{
if(this.drawingCanvas.SelectedItem.GetType() == typeof(DrawingShape))
{
DrawingShape shape = (DrawingShape)this.drawingCanvas.SelectedItem;
TransformGroup transformStack = new TransformGroup();
transformStack.Children.Add(shape.geometryCanvas.LayoutTransform);
transformStack.Children.Add(new RotateTransform(90));
shape.geometryCanvas.LayoutTransform = transformStack;
}
}
Debugging tells me that the contents of shape seem just right. When I execute the command once, shape.geometryCanvas.LayoutTransformis the identity matrix. When executing it a second time, shape.geometryCanvas.LayoutTransform is a TransformGroup of two elements.
It somehow looks like the reference for geometryCanvas (declared in the Xaml) is no the one used on screen.

Got it!
I didn't know that you can't successfully reference x:Name'd XAML elements from outside the code-behind file after de-serialization (that at least seems to be the problem at hand).
A solution is to use FindName() on the UserControl, e.g.:
TextBlock textBox = shape.FindName("TextBox") as TextBlock;
The complete and correct RepairBindingsAfterLoading() looks like this:
private void RepairBindingsAfterLoading()
{
foreach (UIElement element in this.drawingCanvas.Children)
{
if (element.GetType() == typeof(DrawingShape))
{
DrawingShape shape = (DrawingShape)element;
shape.DataContext = shape;
Path path = shape.FindName("Path") as Path;
Binding fillBinding = new Binding("FillBrush");
path.SetBinding(Path.FillProperty, fillBinding);
Binding outlineBinding = new Binding("OutlineBrush");
path.SetBinding(Path.StrokeProperty, outlineBinding);
Binding widthBinding = new Binding("OutlineWidth");
path.SetBinding(Path.StrokeThicknessProperty, widthBinding);
Binding patternBinding = new Binding("OutlinePattern");
path.SetBinding(Path.StrokeDashArrayProperty, patternBinding);
TextBlock textBox = shape.FindName("TextBox") as TextBlock;
Binding labelTextBinding = new Binding("LabelText");
textBox.SetBinding(TextBlock.TextProperty, labelTextBinding);
}
}
}
Just for the record, my clumsy
BindingOperations.ClearAllBindings(shape.Path);
BindingOperations.ClearAllBindings(shape.TextBox);
BindingOperations.ClearAllBindings(shape);
works just like the much more simple and elegant solution suggested by dbaseman with:
shape.DataContext = this;
Hope this helps someone else to avoid my mistake :-)

Related

WPF Bind textbox Text to another control property [duplicate]

I have a situation where I need to create View box with one button. The xaml for this is as below: Please observe Width property of viewbox. The Width should be increased/decreased according to a slider bar(moving to right increases it, to left decreases it). As listed below I know how to do it in xaml and it works fine. But my requirement is to be able to create viewbox in code behind and assign it the properties.
<WrapPanel x:Name="_wrpImageButtons" Grid.IsSharedSizeScope="True"
ScrollViewer.CanContentScroll="True" d:LayoutOverrides="Height"
Margin="5">
<Viewbox x:Name="_ScaleButton"
Width="{Binding Value, ElementName=ZoomSlider}" Stretch="Fill">
<CustomButton:_uscVCARSImagesButton x:Name="_btnImage1"/>
</Viewbox>
</WrapPanel>
Thanks.
This should do what you want:
Viewbox x = new Viewbox();
Binding bnd = new Binding("Value") { ElementName = "ZoomSlider"};
BindingOperations.SetBinding(x, Viewbox.WidthProperty, bnd);
// ... Code to insert the Viewbox into the WrapPanel etc.
You can create the binding relatively easily in Code Behind:
var widthBinding = new Binding("Value") { ElementName = "ZoomSlider" };
_ScaleButton.SetBinding(FrameworkElement.WidthProperty, widthBinding);

Creating a square UserControl

I am designing a user control that needs to be square, and to fill as much room as it is given (to give some context, it is a checkboard).
My user control looks like:
<Grid>
<!-- My 8 lines / colums, etc. , sized with "1*" to have equal lines -->
</Grid>
Now I would simply like to say "This grid has to be square no matter what room it has to expand".
Tried Solutions in vain:
I can't use a UniformGrid because I actually have the names of the lines & columns in addition, so I have a leading header row and column with different sizes.
If I use a Viewbox with Uniform it messes all up.
I tried using the classic
<Grid Height="{Binding RelativeSource={RelativeSource Self}, Path=Width}"> ... </Grid>
but it only works if I manually set the Width property. Otherwise, this constraint is ignored.
Conclusion
I'm out of idea, and I would really like to avoid setting Width / Height manually as this control may be used in many various places (ListItem templates, games, etc...).
Solution from suggestion:
A solution is available with some code-behind. I did not find a XAML only solution.
Grid is now:
<Grid SizeChanged="Board_FullControlSizeChanged">...</Grid>
And the event handler is:
private void Board_FullControlSizeChanged(object sender, SizeChangedEventArgs args)
{
double size = Math.min (args.NewSize.Height, args.NewSize.Width);
((Grid)sender).Width = size;
((Grid)sender).Height = size;
}
I initially tried modifying your binding to ActualWidth and it still did not work when the Grid was the top level element and in some cases it ended up expanding the control further than the available size. Hence tried some other ways of getting the required output.
Got 2 ways of maybe addressing this:
Since this is a view related issue (not breaking MVVM, keeping a square formation, if your ok with having a bit of code-behind, you could do something like)
private void OnSizeChanged(object sender, SizeChangedEventArgs e) {
double minNewSizeOfParentUserControl = Math.Min(e.NewSize.Height, e.NewSize.Width);
mainGrid.Width = minNewSizeOfParentUserControl;
mainGrid.Height = minNewSizeOfParentUserControl;
}
and in your xaml you would name your main top level grid "mainGrid" and attach the UserControl size changed event handler to the above function not the Grid itself.
However if you totally hate code-behind for whatever reason, you can be a bit more fancy and create a behavior such as
public class GridSquareSizeBehavior : Behavior<Grid> {
private UserControl _parent;
protected override void OnAttached() {
DependencyObject ucParent = AssociatedObject.Parent;
while (!(ucParent is UserControl)) {
ucParent = LogicalTreeHelper.GetParent(ucParent);
}
_parent = ucParent as UserControl;
_parent.SizeChanged += SizeChangedHandler;
base.OnAttached();
}
protected override void OnDetaching() {
_parent.SizeChanged -= SizeChangedHandler;
base.OnDetaching();
}
private void SizeChangedHandler(object sender, SizeChangedEventArgs e) {
double minNewSizeOfParentUserControl = Math.Min(e.NewSize.Height, e.NewSize.Width);
AssociatedObject.Width = minNewSizeOfParentUserControl;
AssociatedObject.Height = minNewSizeOfParentUserControl;
}
}
For the behavior your xaml would then look like:
<Grid>
<i:Interaction.Behaviors>
<local:GridSquareSizeBehavior />
</i:Interaction.Behaviors>
</Grid>
Did test these two methods with Snoop and the square size was maintained while expanding/shrinking. Do note both methods in the crux use the same logic(just a quick mock-up) and you might be able to squeeze some better performance if you update the logic to only update height when width is changed and vice versa instead of both and canceling a resize all together if not desired
Try putting your grid in a ViewBox: http://msdn.microsoft.com/en-us/library/system.windows.controls.viewbox.aspx
Here's a code sample I came up with:
The usercontrol:
<UserControl x:Class="StackOverflow.CheckBoard"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Viewbox>
<Grid Background="Red" Height="200" Width="200">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="testing" Grid.Row="0"/>
<Button Content="testing" Grid.Row="1"/>
<Button Content="testing" Grid.Row="2"/>
</Grid>
</Viewbox>
</UserControl>
And the main window:
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:AllNoneCheckboxConverter x:Key="converter"/>
</Window.Resources>
<Grid>
<StackPanel>
<local:CheckBoard MaxWidth="80"/>
</StackPanel>
</Grid>
</Window>
What this Viewbox will do is scale the control to the space it's given. Since the grid inside the viewbox is square, the grid will ALWAYS stay square. Try playing around with the MaxWidth property I used in the MainWindow.
You could bind the Height property to ActualWidth instead of Width:
<Grid Height="{Binding ActualWidth, RelativeSource={RelativeSource Self}}">
...
</Grid>
However, a better solution would be to use a Viewbox. The trick to avoid that it "messes all up" is to make its Child square by defining (sensible) equal values for Width and Height:
<Viewbox>
<Grid Width="500" Height="500">
...
</Grid>
</Viewbox>
Register wherever it suits you (usually in constructor or OnAttached()):
SizeChanged += Square;
and handle size with this:
private void Square(object sender, SizeChangedEventArgs e)
{
if (e.HeightChanged) Width = e.NewSize.Height;
else if (e.WidthChanged) Height = e.NewSize.Width;
}
I have solved this by setting the margin of the contained control from inside the parent control's size changed event.
In my case I have a 'sudoku grid' user control called SudokuBoard inside a standard Grid control called MainGrid (which fills the main window) and it only requires the following code;
private void MainGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
double verticalMargin = Math.Max((e.NewSize.Height - e.NewSize.Width)*0.5, 0.0);
double horizontalMargin = Math.Max((e.NewSize.Width - e.NewSize.Height)*0.5, 0.0);
SudokuBoard.Margin = new Thickness(horizontalMargin, verticalMargin, horizontalMargin, verticalMargin);
}

binding to width property in code behind

I have a situation where I need to create View box with one button. The xaml for this is as below: Please observe Width property of viewbox. The Width should be increased/decreased according to a slider bar(moving to right increases it, to left decreases it). As listed below I know how to do it in xaml and it works fine. But my requirement is to be able to create viewbox in code behind and assign it the properties.
<WrapPanel x:Name="_wrpImageButtons" Grid.IsSharedSizeScope="True"
ScrollViewer.CanContentScroll="True" d:LayoutOverrides="Height"
Margin="5">
<Viewbox x:Name="_ScaleButton"
Width="{Binding Value, ElementName=ZoomSlider}" Stretch="Fill">
<CustomButton:_uscVCARSImagesButton x:Name="_btnImage1"/>
</Viewbox>
</WrapPanel>
Thanks.
This should do what you want:
Viewbox x = new Viewbox();
Binding bnd = new Binding("Value") { ElementName = "ZoomSlider"};
BindingOperations.SetBinding(x, Viewbox.WidthProperty, bnd);
// ... Code to insert the Viewbox into the WrapPanel etc.
You can create the binding relatively easily in Code Behind:
var widthBinding = new Binding("Value") { ElementName = "ZoomSlider" };
_ScaleButton.SetBinding(FrameworkElement.WidthProperty, widthBinding);

WPF: Handling Modal Dialogs

I found a nice solution on this webiste http://www.thesilvermethod.com/Default.aspx?Id=ModalDialogManagerAsimpleapproachtodealingwithmodaldialogsinMVVM
But had to do some changes to get it integrated into my code. Along the way I get some small problems mostly because there are certain parts of the code I'm not getting completely.
How I did it was to bind the ModalDialogManager to a MainWindow property of the Type IDialogViewModel. I then have a WindowsManager class that handles putting the right instance inside this property. One such is EditDialogViewModel that exposes a EditableViewModel to this DialogManager. I set the EditDialog view as a DataTemplate for this EditDialogViewModel but when I show it the new window only shows a part of it.
Here is the View:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="EditDataTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="7*" />
<RowDefinition Height="2*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<ContentControl Content="{Binding Path=ViewModel}" />
<TextBlock Text="{Binding Path=ViewModel.Error}" />
<UniformGrid Grid.Row="2" Columns="2">
<Button Command="{Binding SaveCommand}" />
<Button Command="{Binding CancelCommand}" />
</UniformGrid>
</Grid>
</UserControl>
But the new Dialog window only shows the ContentControl bound to the ViewModel property of EditDialogViewModel (it holds the ViewModel being edited).
My guess is it has something to do with this code in the ModelDialogManager:
void Show()
{
if (_window != null) Close();
Window w = new Window();
_window = w;
w.Closing += w_Closing;
w.Owner = GetParentWindow(this);
w.DataContext = this.DataContext;
w.SetBinding(Window.ContentProperty, ""); //This code here does something I don't fully understand
w.Title = Title;
w.Icon = Icon;
w.Height = DialogHeight;
w.Width = DialogWidth;
w.ResizeMode = DialogResizeMode;
w.ShowDialog();
}
He is applying the binding there but I guess it's only the first ContentControl that gets bound or something. It's quite tricky.
Another problem is that the mouse just doesn't work inside the Modal Dialog. I can tab into the textboxes but not click into them.
Is there a way to fix this or a better method to handle Modal Dialog boxes in WPF?
EDIT
Ok I'm going to admit it. I'm a huge idiot. This was so simple I just couldn't see it. I had set Height and Width on the UserControl to a fixed value while I was still messing around with it being a Window. So in actuality it was showing the whole thing, there just wasn't room. I have no idea why the mouse didn't work at that point but now it works perfectly.
Answering "a better method to handle Modal Dialog boxes in WPF?" there is a new control called Child Window in the WPF Extended Toolkit that addresses your Modal Dialog pains.

Can you override just part of a control template in silverlight

Is it possible to change or modify a specific part of a control template without having to recreate the entire control template of the control in the xaml?
For example, I was trying to get rid of the border of a textbox, so I could throw together a basic search box with rounded corners (example xaml below). Setting the borderthickness to 0 works fine, until you mouse over the textbox and a pseudo border they added to the control flashes up. If I look at the controltemplate for the textbox, I can even see the visual state is named, but cannot think of how to disable it.
Without overriding the control template of the TextBox, how would I stop the Visual State Manager firing the mouse over effect on the TextBox?
<Border Background="White" CornerRadius="10" VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="3" BorderBrush="#88000000">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Width="200" Margin="5,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Path Height="13" Width="14" Stretch="Fill" Stroke="#FF000000" StrokeThickness="2" Data="M9.5,5 C9.5,7.4852815 7.4852815,9.5 5,9.5 C2.5147185,9.5 0.5,7.4852815 0.5,5 C0.5,2.5147185 2.5147185,0.5 5,0.5 C7.4852815,0.5 9.5,2.5147185 9.5,5 z M8.5,8.4999971 L13.5,12.499997" />
<TextBox GotFocus="TextBox_GotFocus" Background="Transparent" Grid.Column="1" BorderThickness="0" Text="I am searchtext" Margin="5,0,5,0" HorizontalAlignment="Stretch" />
</Grid>
</Border>
I've found a way to do this, by inheriting off the control and overriding the OnApplyTemplate. It's not ideal, but I think it's better than having to copy the entire control template. Here's an example of creating a borderless textbox, essentially disabling the mouse over visual state by always clearing the storyboard:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace SilverlightTestApplication
{
public class BorderlessTextBox : TextBox
{
public BorderlessTextBox()
{
BorderThickness = new System.Windows.Thickness(0);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//Get the mouse over animation in the control template
var MouseOverVisualState = GetTemplateChild("MouseOver") as VisualState;
if (MouseOverVisualState == null)
return;
//get rid of the storyboard it uses, so the mouse over does nothing
MouseOverVisualState.Storyboard = null;
}
}
}
Its been awhile since I used XAML, but no, I don't believe you can just modify a piece of the template.
However if you have the IDE you can create a copy of the currently applied template and just modify the piece you want and leave the rest as is. See the How to Edit section of this link.
Retrieve the default template of every control, with the XAML reader, then copy/paste and modify what you want... not very clean but I think this is the only way (I'm searching how to retrieve this default template)
In WPF, not sure about silverlight here a snippet of code to retrieve the template of Aero, you can try to copy/paste and change what you want:
public Window1()
{
InitializeComponent();
using(FileStream fs = new FileStream("C:/TextBoxTemplate.xaml", FileMode.Create))
{
var res = LoadThemeDictionary(typeof(TextBox), "Aero", "NormalColor");
var buttonRes = res[typeof(TextBox)];
XamlWriter.Save(buttonRes, fs);
}
}
public static ResourceDictionary LoadThemeDictionary(Type t,
string themeName, string colorScheme)
{
Assembly controlAssembly = t.Assembly;
AssemblyName themeAssemblyName = controlAssembly.GetName();
object[] attrs = controlAssembly.GetCustomAttributes(
typeof(ThemeInfoAttribute), false);
if(attrs.Length > 0)
{
ThemeInfoAttribute ti = (ThemeInfoAttribute)attrs[0];
if(ti.ThemeDictionaryLocation ==
ResourceDictionaryLocation.None)
{
// There are no theme-specific resources.
return null;
}
if(ti.ThemeDictionaryLocation ==
ResourceDictionaryLocation.ExternalAssembly)
{
themeAssemblyName.Name += "." + themeName;
}
}
string relativePackUriForResources = "/" +
themeAssemblyName.FullName +
";component/themes/" +
themeName + "." +
colorScheme + ".xaml";
Uri resourceLocater = new System.Uri(
relativePackUriForResources, System.UriKind.Relative);
return Application.LoadComponent(resourceLocater)
as ResourceDictionary;
}
I 've never used Silverlight, but I don't think there is a lot of things to do to adapt this template to Silverlight.
Source : Default template in WPF

Resources