WPF Binding control to another control - c#

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.

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.

Dynamic UI Layout in DataGridCell using ItemsControl and a Button

This screenshot is from the mockup of my ideal UI. Right now, this is a DataGridTemplateColumn, with header = "ATTENDEES". I am running into issues creating the layout of this DataGridColumn's cell.
I currently have an ItemsControl bound to a List of strings which are the attendees' emails. If there are too many attendees and the ItemsControls' bounds cannot fit in the cell, then a Button with Content = "See more" should appear at the bottom of the cell, under the last attendee email that can be rendered within in the cell's bounds.
Then once the Button ("See more") is clicked, the row should expand to an appropriate height for the attendees to all be visible, and the "See more" Button should disappear.
I could not wrap my head around a clean implementation with a TemplateSelector, ValueConverter, or DataTrigger in pure XAML since I need to compare the ItemsControls' height against the DataGridRow's height and then perform a modification of the cell's layout at runtime by hiding all the items in the ItemsControl that cannot fit within the cell and then showing at Button below it.
I concluded on attempting to do this in the code-behind by subscribing to the ItemControls' load event. I first attempted to use the Height, MaxHeight, DesiredSize.Height, RenderedSize.Height, and ActualSize.Height properties of the ItemsControl but those all were equal to the clipped height of the ItemsControl, not the intrinsic height of all its contents.
I am now measuring the total height of all its items' strings using the FormattedText class. Then I compare this summed height with the row's height and that's as far as I have progressed; I am unsure of how to next change the layout of the cell or if this is even the correct approach.
I feel like I am fighting against the design of the WPF framework by doing rudimentary calculations and crude layout changes to the view in the code-behind.
Any help on this would be greatly appreciated!
Here is my event handler for the ItemsControl.Load:
private void AttendeesItemsControl_Loaded(object sender, RoutedEventArgs e)
{
if (currentRowIndex == -1)
{
return;
}
List<ModelBase> eventsData = ModelManager.events.data;
var eventObj = (Event)eventsData[currentRowIndex];
var attendees = eventObj.attendees;
var totalItemsHeight = 0;
for(int i = 0; i < attendees.Count; i++)
{
totalItemsHeight += heightOfString(attendees[i]);
}
var itemsControl = (ItemsControl)sender;
var controlRenderHeight = itemsControl.RenderSize.Height;
// Check if the intrinsic height is greater than what can be drawn inside the cell
if (controlRenderHeight < totalItemsHeight)
{
var itemHeight = totalItemsHeight / attendees.Count;
var visibleItemsCount = controlRenderHeight / itemHeight;
// .... not sure how to proceed
}
}
And the helper function that measures the height of one of its items:
private int heightOfString(string candidate)
{
var fontFamily = new FontFamily("Lato");
var fontStyle = FontStyles.Normal;
var fontWeight = FontWeights.Normal;
var fontStretch = FontStretches.Normal;
var fontSize = 12;
var typeFace = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
var formattedText = new FormattedText(candidate, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black);
return (int)formattedText.Height;
}
Finally, this is the DataGridTemplateColumn's XAML, with the cell template definition:
<DataGridTemplateColumn Header="ATTENDEES" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Path=attendees}" x:Name="AttendeesItemsControl" Loaded="AttendeesItemsControl_Loaded">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily="Lato" FontSize="12" FontWeight="Normal" Text="{Binding}">
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
I had to do some real work but I got this set up. Hopefully you can follow it. Here is a screen shot of what it looks like. Obviously i didn't attempt to style it yet. Just getting the resizing. This way you let WPF handle the height of your control you leave it autosized. You just manage your list.
I created a control for the list called AttendeeListControl
<UserControl xmlns:stackoverflow="clr-namespace:stackoverflow" x:Class="stackoverflow.AttendeeListControl"
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 Background="GhostWhite">
<Grid.RowDefinitions>
<RowDefinition Height="37"/>
<RowDefinition Height="*"/>
<RowDefinition Height="23"/>
</Grid.RowDefinitions>
<Label Content="Attendees" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<ListBox Name="listBoxAttendees" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" />
<Button Content="SeeMore" Name="lblMore" HorizontalAlignment="Left" Margin="10,0,0,0" Grid.Row="2" VerticalAlignment="Top" Click="lblMore_Click"/>
</Grid>
</UserControl>
This is the code behind
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Controls;
namespace stackoverflow
{
/// <summary>
/// Interaction logic for AttendeeListControl.xaml
/// </summary>
///
public partial class AttendeeListControl : UserControl
{
public AttendeeListViewModel vm { get; set; }
public AttendeeListControl()
{
InitializeComponent();
var emails = new List<string>() { "email#gmail.com", "email#aol.com", "email.yahoo.com", "email#msn.com" };
var displayed = new ObservableCollection<string>() { emails[0], emails[1] };
vm = new AttendeeListViewModel()
{
EmailList = emails,
DisplayList = displayed,
Expanded = false
};
DataContext = vm;
listBoxAttendees.ItemsSource = vm.DisplayList;
}
private void lblMore_Click(object sender, System.Windows.RoutedEventArgs e)
{
if (vm.Expanded)
{
//remove all but last 2
do
{
vm.DisplayList.RemoveAt(vm.DisplayList.Count - 1);
} while (vm.DisplayList.Count > 2);
lblMore.Content = "Show More";
}
else
{
//don't want the first 2
for (int i = 2; i < vm.EmailList.Count; i++)
{
vm.DisplayList.Add(vm.EmailList[i]);
}
lblMore.Content = "Show Less";
}
vm.Expanded = !vm.Expanded;
}
}
}
and here is the model i used
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace stackoverflow
{
public class AttendeeListViewModel
{
public bool Expanded { get; set; }
public List<string> EmailList { get; set; }
public ObservableCollection<string> DisplayList { get; set; }
}
}
this was all just put on the mainwindow
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:stackoverflow" x:Class="stackoverflow.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:AttendeeListControl HorizontalAlignment="Left" Margin="55,53,0,0" VerticalAlignment="Top"/>
<local:AttendeeListControl HorizontalAlignment="Left" Margin="340,53,0,0" VerticalAlignment="Top"/>
</Grid>
</Window>

How to select combobox item automatically and programatically?

I have an array of combo boxes, and once each combo box is populated with items, I want the first item to be selected automatically. SO I do this:
all_transition_boxes[slide_item].SelectedItem = all_transition_boxes[slide_item].Items[0];
but then later I can not change the index anymore if I want to select some other item. It seems that the index is permanently set to zero. I tried to use SelectedItem instead of SelectedIndex but it doesn't work at all. I would appreciate any help.
//populate each combobox with corresponding elements
for (int i = 0; i < slide_transitions.Count; i++)
{
all_transition_boxes[slide_item].Items.Add("Transition " + (i + 1));
}
all_transition_boxes[slide_item].SelectedItem = all_transition_boxes[slide_item].Items[0];
I have created a sample code to replicate your issue, please check it.
A form with a combobox and two buttons:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Activated="Window_Activated">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="329*"/>
<ColumnDefinition Width="34*"/>
<ColumnDefinition Width="154*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="comboBox" HorizontalAlignment="Left" Margin="146,78,0,0" VerticalAlignment="Top" Width="120"/>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="101,185,0,0" VerticalAlignment="Top" Width="75" Click="button_Click"/>
<Button x:Name="button1" Content="Button" HorizontalAlignment="Left" Margin="266,185,0,0" VerticalAlignment="Top" Width="75" Grid.ColumnSpan="2" Click="button1_Click"/>
</Grid>
</Window>
And the formĀ“s code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Activated(object sender, EventArgs e)
{
var items = new List<string>();
for (var i = 0; i < 10; i++)
{
items.Add("Item" + i);
}
comboBox.ItemsSource = items;
comboBox.SelectedItem = "Item0";
}
private void button_Click(object sender, RoutedEventArgs e)
{
comboBox.SelectedItem = "Item5";
}
private void button1_Click(object sender, RoutedEventArgs e)
{
comboBox.SelectedItem = "Item9";
}
}
}
On your ComboBox (in XAML) set:
SelectedIndex = "0"
You can set it as a setter in a style that is applied to all instances of ComboBox in your array.

WPF databinding a user control

Ive been looking for hours on a solution to this.
I have a tab Adapter class that im using to fill a tab control
public partial class TabAdapter : UserControl
{
public static readonly DependencyProperty fileNameProperty =
DependencyProperty.Register(
"fileName",
typeof(string),
typeof(TabAdapter),
new FrameworkPropertyMetadata(
string.Empty,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnFileNamePropertyChanged),
new CoerceValueCallback(coerceFileName)
),
new ValidateValueCallback(fileNameValidationCallback)
);
public TabAdapter()
{
InitializeComponent();
//initializeInterior();
CreateSaveCommand();
TabAdapterContent.DataContext = this;
Console.WriteLine("constructor hit.");
}
public string fileName
{
get { return (string)GetValue(fileNameProperty); }
set { SetValue(fileNameProperty, value); }
}
private ColumnMapper _columnMap;
private TableMapper _tableMap;
private TabType tabType;
private enum TabType { TABLE_MAPPER, COLUMN_MAPPER, ERROR_MSG }
private static object coerceFileName(DependencyObject d, object value)
{
return fileName;
}
private static bool fileNameValidationCallback(object Value)
{
string fn = (string)Value;
if (fn.Equals(string.Empty))
{
return true;
}
FileInfo fi = new FileInfo(fn);
return ((fi.Exists && fi.Extension.Equals(".csv")));
}
private static void OnFileNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
TabAdapter source = d as TabAdapter;
Console.WriteLine("got to property changer: " + (string)args.NewValue + " :new / old: " + (string)args.OldValue);
source.initializeInterior();
}
private void initializeInterior()
{
Console.WriteLine("initializing Interior filename: " + fileName);
if (Regex.IsMatch(fileName, #".*_SourceTableMapping.csv$"))
{
tabType = TabType.TABLE_MAPPER;
_tableMap = new TableMapper(fileName);
Grid.SetRow(_tableMap, 0);
Grid.SetColumn(_tableMap, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(_tableMap);
}
else if (fileName.EndsWith(".csv"))
{
tabType = TabType.TABLE_MAPPER;
_columnMap = new ColumnMapper(fileName);
Grid.SetRow(_columnMap, 0);
Grid.SetColumn(_columnMap, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(_columnMap);
}
else
{
tabType = TabType.ERROR_MSG;
TextBlock tb = new TextBlock();
tb.Text = "The File: " + fileName + " is not a valid mapping file.";
Grid.SetRow(tb, 0);
Grid.SetColumn(tb, 0);
//clear out the content.
this.TabAdapterContent.Children.Clear();
//add new content
this.TabAdapterContent.Children.Add(tb);
}
}
}
The point of this is to decide what type of file is being added and load up the correct user control inside of it to display that file.
my main window xaml for the tab control is
<TabControl x:Name="tabControl" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding openFileNames, Mode=OneWay}">
<TabControl.LayoutTransform>
<!-- Allows to zoom the control's content using the slider -->
<ScaleTransform CenterX="0"
CenterY="0" />
<!-- part of scale transform ScaleX="{Binding ElementName=uiScaleSlider,Path=Value}"
ScaleY="{Binding ElementName=uiScaleSlider,Path=Value}" />-->
</TabControl.LayoutTransform>
<!-- Header -->
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<!-- Content -->
<TabControl.ContentTemplate>
<DataTemplate>
<local:TabAdapter fileName="{Binding fileName}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
the header works, and if i changed
<local:TabAdapter fileName="{Binding fileName}" />
into
<TextBlock Text="{Binding fileName}" />
Then it all binds correctly, I have a feeling it has something to do with the data context on my tab adapter. but not exactly sure what it needs to be set to.
my xaml for the tab adapter is
<UserControl x:Class="ImportMappingGui.TabAdapter"
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 x:Name="TabAdapterContent">
<Grid.RowDefinitions>
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
it will all compile and run, but only the constructor of the user control gets hit, and at that only once no matter how many tabs I create.
This is my first WPF application so my apologies if it is something stupid im missing.
(or if my methodology of setting up a adapter of sorts is not the best way of solving this issue).
Do not set the DataContext of a UserControl from within the UserControl itself. It defeats the whole point of having "lookless" controls that can be used to display whatever you pass it.
Your fileName binding is failing because the DataContext = this, and this is the TabAdapter control itself, and TabAdapter.fileName is not actually set to anything. (Remember, binding to tell a property to retrieve it's value somewhere else is different from setting the value directly)
As for the constructor not running more than once, that is by design. Since you told the TabControl to use the TabAdapter control as a template, it will create one copy of the TabAdapter, and simply replace the .DataContext behind the control whenever you switch tabs. This increases performance as it doesn't have to keep initializing and tracking a separate control for each tab, and reduces the memory used.

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

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.

Categories

Resources