Windows Phone 8.1 - XAML C# - VisualTreeHelper doesn't find DataTemplate Controls - c#

I'm trying to build an app that displays in a Pivot informations about several products such as their pictures. Each PivotItem is concerning one product and contains (between other controls) another Pivot where I load the pictures of the product in code behind.
Here's the XAML part :
<Page
x:Class="Inventaire.Fenetres.FicheProduit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Inventaire"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<ResourceDictionary>
<DataTemplate x:Key="ProductPivotItem">
<Grid x:Name="rootGrid" Loaded="rootGrid_Loaded">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="productName" Text="{Binding article.name}" FontSize="18"
FontWeight="Bold" HorizontalAlignment="Center" TextWrapping="Wrap"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"/>
<Pivot x:Name="picturesPivot" HorizontalAlignment="Center" Margin="0,5,0,5"
VerticalContentAlignment="Top" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
<!--
Some other controls
-->
</Grid>
</DataTemplate>
</ResourceDictionary>
</Page.Resources>
<Pivot x:Name="productPivot" ItemsSource="{Binding}"
ItemTemplate="{StaticResource ProductPivotItem}" />
</Page>
For the moment I load images in the rootGrid_Loaded event, using VisualTreeHelper to get picturesPivot with a method i found there.
Here's the C# extracts :
private void rootGrid_Loaded(object sender, RoutedEventArgs e)
{
Grid rootGrid = (Grid)sender;
// FindArticle is a method I wrote to get the product (of class Article) concerned by
// the productPivotItem, according to me irrelevant for my problem
Article art = FindArticle(rootGrid);
Pivot picturesPivot = (Pivot)FindChildControl<Pivot>(rootGrid, "picturesPivot");
loadImage(picturesPivot, art);
}
private async void loadImage(Pivot picturesPivot, Article article)
{
for (int i = 0 ; i < article.images.Count ; i++)
{
// ImageProduit is the class gathering infomations I need to build the picture url
// the images property of the Article class is the collection of ImageProduit for the product
// and I use AppelWebService (pattern singleton) to get the image from web
ImageProduit picture = article.images[i];
BitmapImage bmpImage = await AppelWebService.getInstance().loadImage(picture.ToString());
Image img = new Image();
img.Source = bmpImage;
PivotItem pi = new PivotItem();
pi.Content = img;
picturesPivot.Items.Add(pi);
}
}
private DependencyObject FindChildControl<T>(DependencyObject control, string ctrlName)
{
DependencyObject result = null;
bool done = false;
int i = 0;
int childNumber = VisualTreeHelper.GetChildrenCount(control);
while (i < childNumber && !done)
{
DependencyObject child = VisualTreeHelper.GetChild(control, i);
FrameworkElement fe = child as FrameworkElement;
if (fe == null)
{
done = true;
}
else if (child is T && fe.Name == ctrlName)
{
result = child;
done = true;
}
else
{
DependencyObject nextLevel = FindChildControl<T>(child, ctrlName);
if (nextLevel != null)
{
result = nextLevel;
done = true;
}
}
i++;
}
return result;
}
I modified a bit the FindChildControl method in order to have only one return at the end of the method.
Wrote like this I have no problems loading images.
But, sliding on many products, i discover that after around 70 productPivotItem loaded my emulator crash for OutOfMemoryException.
So I want to try to clear picturesPivot.Items when leaving the corresponding productPivotItem to see if it solve the memory problem.
For this I thought use the PivotItemLoaded and PivotItemUnloaded events on productPivot, load images on load and clear the picturesPivot items collection on unload.
Unfortunately I am not able to get back the picturesPivot in these event methods.
Here's what I tried :
private void productPivot_PivotItemLoaded(Pivot sender, PivotItemEventArgs args)
{
// Next three lines independently
args.Item.UpdateLayout();
sender.UpdateLayout();
UpdateLayout();
Pivot picturesPivot = (Pivot)FindChildControl<Pivot>(args.Item, "picturesPivot");
}
Debuging step by step I saw that args.Item has one child, a Grid without name that has himself one child, a ContentPresenter. This ContentPresenter has no child and I can't get any of my controls defined in the DataTemplate.
How could I find them ? I really need you as I had tearing out on this for too long. I hope I was clear enough, the Pivot Inside Pivot thing can be confusing.

Related

How to make a control snap to a Grid.Row/Grid.Column in WPF at runtime?

I have a grid with some ColumnDefinitions and RowDefinitions. What I like to do is drag a control at runtime and have it snap to a given GridColumn/GridRow when the control is over that GridColumn/GridRow. I was not able to find any resources on this. Perhaps I am using the wrong key words. Thanks in advance!
You should extend Grid to handle the drop position. Let the Grid add the dropped element to the appropriate cell.
The following simple but working example shows how to enable dragging of any UIElement from a Panel such as StackPanel or Grid to the custom DrockingGrid.
The custom Grid simply overrides the relevant drag&drop overrides. It's a minimal but working example, therefore only OnDragEnter and OnDrop are overridden.
On drop, you basically have to identify the cell the element was dropped in by using the drop position from the DragEventArgs. Then remove the dropped element from its original parent container (where the drag operation has started) and then insert it into the DockingGrid. You then use Grid.Row and Grid.Column to position the element in the appropriate cell:
DockingGrid.cs
public class DockingGrid : Grid
{
private bool AcceptsDrop { get; set; }
private Brush OriginalBackgroundBrush { get; set; }
public DockingGrid()
{
this.AllowDrop = true;
}
protected override void OnDragEnter(DragEventArgs e)
{
base.OnDragEnter(e);
e.Effects = DragDropEffects.None;
this.AcceptsDrop = e.Data.GetDataPresent(typeof(UIElement));
if (this.AcceptsDrop)
{
e.Effects = DragDropEffects.Move;
ShowDropTargetEffects();
}
}
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragEnter(e);
ClearDropTargetEffects();
}
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
if (!this.AcceptsDrop)
{
return;
}
ClearDropTargetEffects();
var droppedElement = e.Data.GetData(typeof(UIElement)) as UIElement;
RemoveDroppedElementFromDragSourceContainer(droppedElement);
_ = this.Children.Add(droppedElement);
Point dropPosition = e.GetPosition(this);
SetColumn(droppedElement, dropPosition.X);
SetRow(droppedElement, dropPosition.Y);
}
private void SetRow(UIElement? droppedElement, double verticalOffset)
{
double totalRowHeight = 0;
int targetRowIndex = 0;
foreach (RowDefinition? rowDefinition in this.RowDefinitions)
{
totalRowHeight += rowDefinition.ActualHeight;
if (totalRowHeight >= verticalOffset)
{
Grid.SetRow(droppedElement, targetRowIndex);
break;
}
targetRowIndex++;
}
}
private void SetColumn(UIElement? droppedElement, double horizontalOffset)
{
double totalColumnWidth = 0;
int targetColumntIndex = 0;
foreach (ColumnDefinition? columnDefinition in this.ColumnDefinitions)
{
totalColumnWidth += columnDefinition.ActualWidth;
if (totalColumnWidth >= horizontalOffset)
{
Grid.SetColumn(droppedElement, targetColumntIndex);
break;
}
targetColumntIndex++;
}
}
private void RemoveDroppedElementFromSourceContainer(UIElement droppedElement)
{
DependencyObject parent = droppedElement is FrameworkElement frameworkElement
? frameworkElement.Parent
: VisualTreeHelper.GetParent(droppedElement);
if (parent is null)
{
return;
}
switch (parent)
{
case Panel panel:
panel.Children.Remove(droppedElement);
break;
case ContentControl contentControl:
contentControl.Content = null;
break;
case ContentPresenter contentPresenter:
contentPresenter.Content = null;
droppedElement.UpdateLayout();
break;
case Decorator decorator:
decorator.Child = null;
break;
default:
throw new NotSupportedException($"Parent type {parent.GetType()} not supported");
}
}
private void ShowDropTargetEffects()
{
this.ShowGridLines = true;
this.OriginalBackgroundBrush = this.Background;
this.Background = Brushes.LightBlue;
}
private void ClearDropTargetEffects()
{
this.Background = this.OriginalBackgroundBrush;
this.ShowGridLines = false;
}
}
Usage
Use it like a normal Grid.
Now the user can drag any control into any of the predefined cells.
<local:DockingGrid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="300" />
<RowDefinition />
</Grid.RowDefinitions>
</local:DockingGrid>
In the parent host of the drag&drop context for example the Window, enable/start the drag behavior:
MainWindow.xaml.cs
partial class MainWindow : Window
{
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed
&& e.Source is UIElement uIElement)
{
_ = DragDrop.DoDragDrop(uIElement, new DataObject(typeof(UIElement), uIElement), DragDropEffects.Move);
}
}
}
See Microsoft Docs: Drag and Drop Overview to learn more about the feature.
The short answer is to put that control inside something which fills that cell. You could just put it in that grid cell by adding it to the grid children and setting grid row and column attached properties but there is a gotcha.
A grid cell is sort of conceptual.
The grid looks at it's content, looks at it's definitions for rows and columns and works out where to put it's content using measure arrange passes.
Which is a wordy way of saying there's nothing there to drag your control into.
You need a drop target to drag drop anything into. As it's name suggests, you need some sort of a receptacle for the thing you are dragging.
Wpf, however has these things called content controls.
A button actually inherits from content control to allow it to have things like a string in it.
There is also a content control itself. Which is just kind of like a receptacle for something or other.
One of these things can be used in a given cell as a sort of a place holder. And then you have something in a cell that you can drop into.
I think if you just throw a contentcontrol in a grid without anything inside it you might have problems hit testing.
Some experimentation in a scratch project is advisable.
But basically you could have something like:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red"
Name="DraggAbleThing"
MouseMove="DraggAbleThing_MouseMove"
/>
<ContentControl Grid.Row="1"
Grid.Column="1"
x:Name="BottomRight"
AllowDrop="True"
>
<Rectangle Fill="Yellow"/>
</ContentControl>
</Grid>
There's a fair bit to implement in order to do drag drop but the idea here is you have something in the bottom right cell which you can drop into. You might have to set ishitestable=false on that yellow rectangle.
I'd have to implement all the drag drop methods to try it out.
If I did and drop works ok then when the contentcontrol gets draggablething dropped into it.
Set the content property of the contentcontrol to draggablething and it is now in the bottom right cell.
It will fill that cell because the grid arranges it's contents to fill whichever logical cell it decides they're "in".
I would like to present an example I wrote that is working.
In the Application I wrote I have a Grid with 4 Rows and 4 Columns.
I can place in each Cell a different UserControl that is based on a class I called
BaseDragDropUserControl:
public class BaseDragDropUserControl: UserControl
{
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed)
{
DataObject data = new DataObject();
data.SetData(DataFormats.StringFormat, nameof(BaseDragDropUserControl));
BaseDragDropUserControl tobemMoved = (BaseDragDropUserControl)e.Source;
int row = (int)tobemMoved.GetValue(Grid.RowProperty);
int col = (int)tobemMoved.GetValue(Grid.ColumnProperty);
data.SetData("Source", tobemMoved);
data.SetData("Row", row);
data.SetData("Col", col);
DragDrop.DoDragDrop(this, data, DragDropEffects.Move);
}
}
protected override void OnGiveFeedback(GiveFeedbackEventArgs e)
{
base.OnGiveFeedback(e);
if (e.Effects.HasFlag(DragDropEffects.Copy))
{
Mouse.SetCursor(Cursors.Cross);
}
else if (e.Effects.HasFlag(DragDropEffects.Move))
{
Mouse.SetCursor(Cursors.Pen);
}
else
{
Mouse.SetCursor(Cursors.No);
}
e.Handled = true;
}
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
// If the DataObject contains string data, extract it.
if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
string dataString = (string)e.Data.GetData(DataFormats.StringFormat);
if (dataString == nameof(BaseDragDropUserControl))
{
int targetRow = (int)this.GetValue(Grid.RowProperty);
int targetCol = (int)this.GetValue(Grid.ColumnProperty);
int originRow = (int)e.Data.GetData("Row");
int originCol = (int)e.Data.GetData("Col");
BaseDragDropUserControl origin = (BaseDragDropUserControl)e.Data.GetData("Source");
this.SetValue(Grid.RowProperty, originRow);
this.SetValue(Grid.ColumnProperty, originCol);
origin.SetValue(Grid.RowProperty, targetRow);
origin.SetValue(Grid.ColumnProperty, targetCol);
}
}
e.Handled = true;
}
}
The above class is the "Heavy one". It handle both the Drag and the Drop functions.
It ships data object with the origin UserControl and also intercept it when it is dropped. It switch the Grid.Row and Grid.Column values between the origin UserControl and the Target UserControl. In doing this the locations are changed.
I created 2 UsserControls.
RedUserControl and BlueUserControl:
<local:BaseDragDropUserControl x:Class="Problem10.RedUserControl"
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:Problem10"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" AllowDrop="True">
<Grid>
<Rectangle Fill="Red"/>
</Grid>
</local:BaseDragDropUserControl>
<local:BaseDragDropUserControl x:Class="Problem10.BlueUserControl"
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:Problem10"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" AllowDrop="True">
<Grid>
<Rectangle Fill="Blue"/>
</Grid>
</local:BaseDragDropUserControl>
The MainWindow is as following:
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<local:RedUserControl Grid.Row="0" Grid.Column="1"/>
<local:BlueUserControl Grid.Row="0" Grid.Column="0"/>
<local:BlueUserControl Grid.Row="0" Grid.Column="2"/>
<local:BlueUserControl Grid.Row="0" Grid.Column="3"/>
<local:RedUserControl Grid.Row="1" Grid.Column="0"/>
<local:BlueUserControl Grid.Row="1" Grid.Column="1"/>
<local:BlueUserControl Grid.Row="1" Grid.Column="2"/>
<local:BlueUserControl Grid.Row="1" Grid.Column="3"/>
<local:RedUserControl Grid.Row="2" Grid.Column="0"/>
<local:BlueUserControl Grid.Row="2" Grid.Column="1"/>
<local:BlueUserControl Grid.Row="2" Grid.Column="2"/>
<local:BlueUserControl Grid.Row="2" Grid.Column="3"/>
<local:RedUserControl Grid.Row="3" Grid.Column="0"/>
<local:BlueUserControl Grid.Row="3" Grid.Column="1"/>
<local:BlueUserControl Grid.Row="3" Grid.Column="2"/>
<local:BlueUserControl Grid.Row="3" Grid.Column="3"/>
</Grid>
</Window>
The Application is ready for you ! Come and play.

WPF "Hidden" behind a Panel

I am encountering an issue in my project and i don't have much more time to do research.
The goal of this project is to allow attending to tests without being physically present.
You receive a file containing your tests, you attend to them (time limited) and send back the file containing your answers.
I got a TextBox in a StackPanel, itself contained in another StackPanel.
All the controls are created programatically.
The controls are added correctly but the TextBox don't react to mouse input.... (in fact only when the textbox is the ast item and even ther only the little last pixel)
UserControl XAML file :
<UserControl x:Class="DataLibrary.View.Questions.ListQuestionInterface"
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:localization ="clr-namespace:DataLibrary.Resources"
xmlns:convert ="clr-namespace:DataLibrary.View.Converters"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Loaded="ListQuestionInterface_OnLoaded">
<UserControl.Resources>
<localization:LocalizedStrings x:Key="LocalizedStrings"/>
<convert:getVisible x:Key="getVisible"/>
<convert:getText x:Key="getText"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" x:Name="body" Grid.Row="0" FocusManager.IsFocusScope="True"/>
<Label Grid.Row="0" Margin="0,10,0,0" x:Name="explanations"/>
<Button Content="{Binding Path=type, Converter={StaticResource getText}}"
HorizontalAlignment="Right"
Margin="0,0,10,10"
VerticalAlignment="Bottom"
Grid.Row="1"
Width="120"
Height="20"
Click="DisplayAnswerButton_Click"
Visibility="{Binding Path=type, Converter={StaticResource getVisible}}"/>
</Grid>
</UserControl>
Code Behind:
public partial class ListQuestionInterface : UserControl
{
private UIElement _firstElement;
ListQuestion q;
private bool isTest;
public questionType type
{
get
{
return q.Type;
}
set
{
Console.WriteLine("Attempted to write questionType");
}
}
public ListQuestionInterface(ListQuestion question, bool isTest = true)
{
InitializeComponent();
this.explanations.Content = question.Explanation;
this.DataContext = this;
this.q = question;
this.isTest = isTest;
refreshStackPanel();
}
private void refreshStackPanel()
{
bool first = true;
this.body.Children.Clear();
var enumerators = new Hashtable();
foreach (Question subQuestion in this.q.SubQuestions)
{
enumerators.Add(subQuestion, subQuestion.interfaceEnumerator(isTest).GetEnumerator());
((IEnumerator)enumerators[subQuestion]).MoveNext();
}
//If the Alignemnt property has a value we'll want each pair of control to be aligned wit heach other
//if not, we just want them stacked to the left
if (q.Alignment.HasValue)
{
int maxCount = this.q.SubQuestions.Max(x => x.interfaceEnumerator(isTest).Count());
for (int i = 0; i < maxCount; i++)
{
var stack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Center
};
foreach (Question subQuestion in this.q.SubQuestions)
{
try
{
var enumerator = (IEnumerator)enumerators[subQuestion];
var control = enumerator.Current as Control;
((Panel)VisualTreeHelper.GetParent(control)).Children.Remove(control);
control.HorizontalAlignment = q.Alignment.Value;
Canvas canvas = null;
if (control.GetType() == typeof(Button) || control.GetType() == typeof(MaskedTextBox))
{
canvas = new Canvas();
if (control.GetType() == typeof(MaskedTextBox))
{
var thick = control.Margin;
thick.Left -= 5;
control.Margin = thick;
}
if (first)
{
this._firstElement = control;
first = false;
}
control.Focusable = true;
canvas.Children.Add(control);
}
if (canvas == null)
{
stack.Children.Add(control);
}
else
{
stack.Children.Add(canvas);
}
enumerator.MoveNext();
}
catch
{
var blank = new Label
{
Content = "BLANK",
Visibility = Visibility.Hidden
};
stack.Children.Add(blank);
Console.WriteLine("No more items to display");
}
}
this.body.Children.Add(stack);
}
}
else
{
var stack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Center,
};
foreach (var subQuestion in q.SubQuestions)
{
var subStack = new StackPanel
{
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Left,
Orientation = Orientation.Horizontal
};
var enumerator = subQuestion.interfaceEnumerator(isTest).GetEnumerator();
while (enumerator.MoveNext())
{
var control = enumerator.Current as Control;
control.HorizontalAlignment = HorizontalAlignment.Left;
if (control.GetType() == typeof(Button) || control.GetType() == typeof(MaskedTextBox))
{
if (first)
{
this._firstElement = control;
first = false;
}
control.Focusable = true;
}
((Panel)VisualTreeHelper.GetParent(control)).Children.Remove(control);
subStack.Children.Add(control);
}
stack.Children.Add(subStack);
}
this.body.Children.Add(stack);
}
}
private void DisplayAnswerButton_Click(object sender, RoutedEventArgs e)
{
foreach (Question question in q.SubQuestions)
{
question.DisplayAnswers();
}
refreshStackPanel();
}
private void ListQuestionInterface_OnLoaded(object sender, RoutedEventArgs e)
{
this._firstElement.Focus();
}
}
}
I don't think the button have somethng to do with the problem so i'll leave the converters code away.
I've checked some things already :
IsHitTestVisible is never set to false
Defining one of the stackpanels as the FocusScope don't change anything
If i place all my controls into canvas and the canvas into the stackpanel i can freely click on my controls but their placement is completely broken.
The Actual Width/height of the control is sufficient to interact with them(60x20)
The problem seems to have appeared just after using the following trick to set a first focused element on other UserControls (which are not in the current VisualTree anymore)
I really do need help since i can't seem to find someone with a similar problem.
And here are two screenshots to illustrate the problem :
The black arrows shows where my i clicked before taking the screenshot (btw if you know of any software that can do a screenshot WITH the mouse i'm taking it :) )
Ok, my fault here -_-'
I was so tired that i didnt't see that my stackpanel was in fact really BEHIND a Label
In my code i only had 2 row definitions, in the first i put the stackpanel AND a Label (who took the entire space).
And because it was declared later, the label was above the stackpanel thus preventing any mouse interaction with it's content.
Here is the corrected XAML :
<UserControl x:Class="DataLibrary.View.Questions.ListQuestionInterface"
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:localization ="clr-namespace:DataLibrary.Resources"
xmlns:convert ="clr-namespace:DataLibrary.View.Converters"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Loaded="ListQuestionInterface_OnLoaded">
<UserControl.Resources>
<localization:LocalizedStrings x:Key="LocalizedStrings"/>
<convert:getVisible x:Key="getVisible"/>
<convert:getText x:Key="getText"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60*"/>
<RowDefinition Height="20*"/>
<RowDefinition Height="10*"/>
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal" x:Name="body" Grid.Row="0" FocusManager.IsFocusScope="True"/>
<Label Grid.Row="1" Margin="0,10,0,0" x:Name="explanations"/>
<Button Content="{Binding Path=type, Converter={StaticResource getText}}"
HorizontalAlignment="Right"
Margin="0,0,10,10"
VerticalAlignment="Bottom"
Grid.Row="2"
Width="120"
Height="20"
Click="DisplayAnswerButton_Click"
Visibility="{Binding Path=type, Converter={StaticResource getVisible}}"/>
</Grid>
</UserControl>
So in fact, the control was really hidden behind another one -_-'
I now know that this isn't the WPF way of doing things, but i do not know yet how to DataBind to a Template properly
I'm still taking any advices on a good tutorial/starting point for binding to a DataTemplate in XAML, tho only DataBindings i could find were for binding single values to control properties

WPF Binding control to another control

I have the following WPF window. Changing the value of #Cables adds (or removes) tabs to the TabControl (C2, C3, C4...). Changing #Phases adds new rows to the DataGrid.
All of the tabs (other than this Options one) have the same format, so I created a UserControl NTab class which has its own .xaml and code-behind.
Now, each of the other tabs will have a ComboBox, where the user selects the appropriate Phase (by the name property). For that to be possible, the NTab needs to be aware of the Phase DatGrid in the Options tab. My code is currently split into two classes:
MainWindow, which contains the code (.xaml and code-behind) for the window as a whole and for the Options Tab;
NTab, which contains the code (.xaml and code-behind) for itself.
The Phase DataGrid's ItemSource is an ObservableCollection, so what I've done is sent the collection to the NTab constructor and tied its .CollectionChanged event to the following NTab function (Phases is an ObservableCollection<string> dependency property):
public void PhasesChanged(object source, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var pC = source as ObservableCollection<NPhase>;
Phases.Clear();
for (int i = 0; i < pC.Count; i++)
Phases.Add("" + (i + 1));
}
This creates a copy of the DataGrid's data (just the names) and stores it inside each NTab as a dependency property bound to the ComboBox. This works.
My question is if there is a better way to do this. I would rather not have to create "redundant" copies of data and would like to be able to bind the ComboBoxes directly to the DataGrid. Is this possible?
EDIT: Adding my code. I've deleted parts of the .xaml and code-behind which weren't relevant to the question.
MainWindow.xaml
<Window x:Class="WPF.MainWindow"
x:Name="Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:l="clr-namespace:WPF"
Title="WPF" SizeToContent="WidthAndHeight">
<TabControl Name="tabControl" SelectedIndex="1">
<TabItem Name="Options">
<TabItem.Header>
<TextBlock>Options</TextBlock>
</TabItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- ... -->
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<!-- ... -->
</Grid.ColumnDefinitions>
<Label Grid.Column="0"># Cables</Label>
<xctk:IntegerUpDown Name="nCables"
Grid.Column="1"
Minimum="1"
Value="1"
ValueChanged="nCablesChanged"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<!-- ... -->
</Grid.ColumnDefinitions>
<Label Grid.Column="0"># Phases</Label>
<xctk:IntegerUpDown Name="nPhases"
Grid.Column="1"
Minimum="1"
Value="4"
ValueChanged="nPhasesChanged"/>
</Grid>
<l:NDataGrid Grid.Row="2"
x:Name="PhaseGrid"
ItemsSource="{Binding Phases, ElementName=Main}"
LoadingRow="RowIndex"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserSortColumns="False"
VerticalScrollBarVisibility="Hidden"
Block.TextAlignment="Center"/>
</Grid>
</TabItem>
</TabControl>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
ObservableCollection<NPhase> Phases;
public static DependencyProperty PhasesProperty = DependencyProperty.Register("Phases", typeof(ICollectionView), typeof(MainWindow));
public ICollectionView IPhasesCollection
{
get { return (ICollectionView)GetValue(PhasesProperty); }
set { SetValue(PhasesProperty, value); }
}
/// <summary>Controls the number of cables to be created</summary>
/// <param name="sender">(IntegerUpDown)nCables</param>
/// <param name="e">not used</param>
private void nCablesChanged(object sender, RoutedEventArgs e)
{
int n = tabControl.Items.Count - 1;
var o = sender as IntegerUpDown;
int v = (int)o.Value;
if (v > n)
{
for (int i = n; i < v; i++)
{
TabItem tab = new TabItem();
tab.Header = "C" + (i + 1);
tab.Content = new NTab(Phases);
tabControl.Items.Add(tab);
}
}
else if (v < n)
{
for (int i = v; i < n; i++)
tabControl.Items.RemoveAt(n);
}
}
/// <summary>Modifies the DataGrid according to the number of phases desired</summary>
/// <param name="sender">(IntegerUpDown)nPhases</param>
/// <param name="e">not used</param>
private void nPhasesChanged(object sender, RoutedEventArgs e)
{
//...
}
/// <summary>Sets up the row headers</summary>
/// <param name="sender">not used</param>
/// <param name="e">Row to be modified</param>
private void RowIndex(object sender, DataGridRowEventArgs e)
{
e.Row.Header = (e.Row.GetIndex() + 1).ToString();
}
public MainWindow()
{
Phases = new ObservableCollection<NPhase>();
Phases.Add(new NPhase(3, "Protensao Inicial"));
Phases.Add(new NPhase(28, "Carga Movel"));
Phases.Add(new NPhase(365, "1 Ano"));
Phases.Add(new NPhase(18250, "Vida Util"));
IPhasesCollection = CollectionViewSource.GetDefaultView(Phases);
InitializeComponent();
}
}
NTab.xaml
<UserControl x:Class="WPF.NTab"
x:Name="CableTab"
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:charting="clr-namespace:System.Windows.Forms.DataVisualization.Charting;assembly=System.Windows.Forms.DataVisualization"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:l="clr-namespace:WPF"
mc:Ignorable="d" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- ... -->
</Grid.RowDefinitions>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<!-- ... -->
</Grid.ColumnDefinitions>
<Label Grid.Column="2">Pull Phase</Label>
<ComboBox Grid.Column="3"
Name="Phase"
ItemsSource="{Binding Phases, ElementName=CableTab}"/>
</Grid>
</Grid>
</UserControl>
NTab.xaml.cs
public partial class NTab : UserControl
{
ObservableCollection<string> Phases;
public static DependencyProperty PhasesProperty = DependencyProperty.Register("Phases", typeof(ICollectionView), typeof(NTab));
public ICollectionView IPhasesCollection
{
get { return (ICollectionView)GetValue(PhasesProperty); }
set { SetValue(PhasesProperty, value); }
}
/// <summary>Updates the tab's Phase list to match the list in the Options tab.</summary>
/// <param name="source">(ObservableCollection<NPhase></param>
/// <param name="e">not used.</param>
public void PhasesChanged(object source, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var pC = source as ObservableCollection<NPhase>;
Phases.Clear();
for (int i = 0; i < pC.Count; i++)
Phases.Add("" + (i + 1));
}
public NTab(ObservableCollection<NPhase> p)
{
InitializeComponent();
Phases = new ObservableCollection<string>();
IPhasesCollection = CollectionViewSource.GetDefaultView(Phases);
PhasesChanged(p, new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
p.CollectionChanged += PhasesChanged;
}
}
You're looking at this in the wrong way... it is a rare situation in WPF when a UI control needs to know anything about another UI control. It is much more common to work with the data. By this, I mean that your NTab only needs to be aware of the data from the Phase DataGrid and not the control.
The simplest way to achieve this is to have one view model for the whole TabControl which shares its properties over the various TabItems. So rather than trying to handle UI events to keep the collections the same, just use one collection for all of the TabItems. In the DataGrid, you could do this:
<DataGrid ItemsSource="{Binding YourCollection}" ... />
On each additional TabItem, you could have this:
<ComboBox ItemsSource="{Binding YourCollection}" ... />
In this way, an update to the collection in one TabItem will automatically be reflected in all of the other TabItems, without you having to do anything.

Correctly disposing user control and its children

I have been looking for over 30 minutes now, but I simply cannot figure out what the problem is.
I have a TabControl and its items are to be closed by the user. Since each TabItem is in a way connected to a custom control and several objects that each use quite a lot of memory, I would like to dispose all objects that are used together with this TabItem.
To make it clearer and save you a lot of code here the simplified situation:
<UserControl x:Class="myProject.GridFour"
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">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ScrollViewer Height="Auto" Margin="0" Name="scrollViewer11" Width="Auto" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"></ScrollViewer>
<ScrollViewer Grid.Column="1" Height="Auto" Name="scrollViewer12" Width="Auto" Margin="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"></ScrollViewer>
<ScrollViewer Grid.Row="1"> Height="Auto" Name="scrollViewer21" Width="Auto" Margin="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"></ScrollViewer>
<ScrollViewer Height="Auto" Name="scrollViewer22" Width="Auto" Grid.Column="1" Margin="0" Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"></ScrollViewer>
</Grid>
</UserControl>
Now I set the content of the corresponding tabitem:
GridFour myControl = new GridFour();
myTabItem.Content = myControl;
Also I have custom objects that each contain a grid, which is added as content to the scrollviewers of my user control:
class MyClass
{
internal Grid _grid = new Grid();
internal Image _image = new Image() {Width = Double.NaN, Height = Double.NaN HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Stretch = Stretch.Fill};
//... a lot of variables, data, images and methods...
}
MyClass[] myObject = new MyClass[4];
for(int i = 0; i < 4; i++)
{
myObject[i] = new MyClass();
myObject[i]._grid.Children.Add(_image); //that actually happens when I initialise the object, it is just to show how everything is connected
}
myControl.scrollViewer11.Content = myObject[0]._grid;
myControl.scrollViewer12.Content = myObject[1]._grid;
myControl.scrollViewer21.Content = myObject[2]._grid;
myControl.scrollViewer22.Content = myObject[3]._grid;
Now when the user would like to close the tabitem obviously I would also like to get rid of myControl and of every single object myObject.
I tried to call the Dispose method on them via IDisposable but that always throws a NullReferenceException and I simply cannot figure out why.
I should maybe mention that every single myObject is within a Dictionary<string, MyClass> but I remove the object from there before I call dispose.
class MyClass : IDisposable
{
internal Grid _grid = new Grid();
internal Image _image = new Image() {Width = Double.NaN, Height = Double.NaN HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Stretch = Stretch.Fill};
//... a lot of variables, data, images and methods...
public void Dispose()
{
// your custom disposing
_image = null; //or something like that
}
}
So you are removing a UI element and then trying to get the model to update, instead:
Why not bind the UI to a ObservableCollection list of your object. Then to remove you just need to remove from the list in the model (and dispose if you wish) but let the UI do it's job and simply relect the changes to the list. This is how WPF works best.
Read up on TabControl.ItemsSource Like here.

Window SizeToContent and ListBox sizing

I am trying to figure out how to use a WPF list box in conjunction with a window that has its SizeToContent property set to WidthAndHeight. The MainWindow has a grid with two rows and two columns. In the bottom right quadrant, I have a list box. I want the rest of the controls in the window to control the size of the window, and the list box to simply fill the available space. Currently, the list box is expanding to fit its contents, which causes the entire MainWindow to expand.
Note: I attempted to create a simple example, but want to point out that in my real scenario I'm using MVVM, and the controls that I want to determine the window width/height are bound to properties in a viewmodel that have their values set after the window is loaded.
Edit to add: The list box is bound to its content before the controls that I want to determine size are, and I don't have control over that.
Here is what the MainWindow currently looks like at startup:
Notice the red and blue bars which indicate what I'm not wanting to happen. Content in that area should only be visible by scroll bars.
Here is what I want the MainWindow to look like at startup:
Notice the size of the MainWindow is determined by the text blocks along the top and left sides, and the list box fills the available space and uses scrollbars if necessary.
Here is some sample code...
MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="The window should fit to the width of this" FontSize="15"/>
<Canvas Background="Red" Grid.Column="1"/>
</Grid>
<Grid Grid.Row="1" Grid.RowSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="The window should fit to the height of this" FontSize="15">
<TextBlock.LayoutTransform>
<RotateTransform Angle="-90"/>
</TextBlock.LayoutTransform>
</TextBlock>
<Canvas Background="Blue" Grid.Row="1"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="1">
<ListBox Name="ListBox1">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var Messages = new ObservableCollection<string>() { "This is a long string to demonstrate that the list box content is determining window width" };
for (int i = 0; i < 16; i++)
Messages.Add("Test" + i);
for (int i = 0; i < 4; i++)
Messages.Add("this text should be visible by vertical scrollbars only");
ListBox1.ItemsSource = Messages;
}
}
Set the list box ItemsSource, and set SizeToContent=Manual after the Window has loaded.
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
SizeToContent = SizeToContent.Manual;
var messages = new ObservableCollection<string>
{
"This is a long string to demonstrate that the list" +
" box content is determining window width"
};
for (int i = 0; i < 16; i++)
{
messages.Add("Test" + i);
}
for (int i = 0; i < 4; i++)
{
messages.Add("this text should be visible by vertical scrollbars only");
}
ListBox1.ItemsSource = messages;
}
In this way the main window is initially sized to fit the content (with no data in the listbox), and then the listbox displays its items with scrollbars.

Categories

Resources