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.
Related
I am trying to implement a custom control that acts similarly to a standard WrapPanel, but that allows you to specify a header and footer. Visually, this is what I am trying to accomplish:
I have created a custom control that seems to leave room for the header and footer items, but I am unable to get them to visually appear. This is my first attempt at any sort of custom control, so any help or input is appreciated!
C#
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyProject.Extras
{
public class HeaderedFooteredPanel : Panel
{
public FrameworkElement Header
{
get { return (FrameworkElement) GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public FrameworkElement Footer
{
get { return (FrameworkElement)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public static DependencyProperty HeaderProperty = DependencyProperty.Register(
nameof(Header),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
public static DependencyProperty FooterProperty = DependencyProperty.Register(
nameof(Footer),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
protected override Size MeasureOverride(Size constraint)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var measure = new Action<FrameworkElement>(element =>
{
element.Measure(constraint);
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > constraint.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
measure(Header);
foreach (FrameworkElement child in InternalChildren)
{
measure(child);
}
measure(Footer);
return new Size(largestX, largestY);
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var arrange = new Action<FrameworkElement>(element =>
{
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > finalSize.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
element.Arrange(new Rect(new Point(0.0, y), element.DesiredSize));
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
arrange(Header);
foreach (FrameworkElement child in InternalChildren)
{
arrange(child);
}
arrange(Footer);
return new Size(largestX, largestY);
}
}
}
Usage in XAML:
<ItemsControl ItemsSource="{Binding SomeItems}" ItemTemplate="{StaticResource SomeTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<extras:HeaderedFooteredPanel>
<extras:HeaderedFooteredPanel.Header>
<TextBlock Text="Header" />
</extras:HeaderedFooteredPanel.Header>
<extras:HeaderedFooteredPanel.Footer>
<TextBlock Text="Footer" />
</extras:HeaderedFooteredPanel.Footer>
</extras:HeaderedFooteredPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
You write in the comments:
The DrawingContext supplied to the OnRender() method only seems to support very basic rendering commands. Surely you don't have to re-write the rendering code for a standard WPF control, but I am not seeing a way to draw them on my own
If by "basic" you mean you are restricted to only DrawingContext operations, then yes. That's exactly what it's for. That's actually the drawing API for WPF. At a higher level, you are dealing with visuals and framework elements, which hide the actual drawing activity. But to override the way such objects are drawn, will require diving down into that level of drawing, replacing it or supplementing it as necessary.
One significant difficulty that would probably arise (besides the more fundamental difficulty of dealing with drawing at that level) is that at that level, there is no such thing as data templates, and no way to access the rendering behaviors of other elements. You have to draw everything from scratch. This would wind up negating a big part of what makes WPF so useful: convenient and powerful control over the exact on-screen representation of data through the use of built-in controls and the properties that give you control over their appearance.
I have only rarely found that a custom Control sub-class is really needed. The only time this comes up is when you need to have complete control over the entire rendering process, to draw something that is simply not possible any other way, or to provide the required performance (at the expense of convenience). Much more often, nearly all of the time even, what you want to do is leverage the existing controls and get them to do all the heavy lifting for you.
In this particular case, I think the key to solving your problem is a type called CompositeCollection. Just like it sounds, it allows you build up a collection as a composite of other objects, including other collections. With this, you can combine your header and footer data into a single collection that can be displayed by an ItemsControl.
In some cases, just creating that collection and using it directly with an ItemsControl object might be sufficient for your needs. But if you want a whole, reusable user-defined control that understands the idea of a header and footer, you can wrap the ItemsControl in a UserControl object that exposes the properties you need, including a Header and Footer property. Here is an example of what that might look like:
XAML:
<UserControl x:Class="TestSO43008469HeaderFooterWrapPanel.HeaderFooterWrapPanel"
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:TestSO43008469HeaderFooterWrapPanel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl x:Name="wrapPanel1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
C#:
public partial class HeaderFooterWrapPanel : UserControl
{
private const int _kheaderIndex = 0;
private const int _kfooterIndex = 2;
private readonly CompositeCollection _composedCollection = new CompositeCollection();
private readonly CollectionContainer _container = new CollectionContainer();
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kheaderIndex)));
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
"Footer", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kfooterIndex)));
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof(IEnumerable), typeof(HeaderFooterWrapPanel),
new PropertyMetadata(_OnItemsSourceChanged));
private static void _OnHeaderFooterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, int index)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._composedCollection[index] = e.NewValue;
}
private static void _OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._container.Collection = panel.ItemsSource;
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public string Footer
{
get { return (string)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public HeaderFooterWrapPanel()
{
InitializeComponent();
_container.Collection = ItemsSource;
_composedCollection.Add(Header);
_composedCollection.Add(_container);
_composedCollection.Add(Footer);
wrapPanel1.ItemsSource = _composedCollection;
}
}
Noting, of course, that doing it that way, you would need to "forward" all of the various control properties that you want to be able to set, from the UserControl object to the ItemsPanel. Some, like Background, you can probably just set on the UserControl and have the desired effect, but others are specifically applicable to the ItemsControl, like ItemTemplate, ItemTemplateSelector, etc. You'll have to figure out which those are, and bind the properties, with the source being the UserControl and the target the ItemsControl inside, declaring as dependency properties in your UserControl class any that aren't already part of the UserControl type.
Here's a little sample program that shows how the above could be used:
XAML:
<Window x:Class="TestSO43008469HeaderFooterWrapPanel.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:l="clr-namespace:TestSO43008469HeaderFooterWrapPanel"
xmlns:s="clr-namespace:System;assembly=mscorlib"
DataContext="{Binding RelativeSource={x:Static RelativeSource.Self}}"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<TextBlock Text="Header: "/>
<TextBox Text="{Binding Header, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<TextBlock Text="Footer: "/>
<TextBox Text="{Binding Footer, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<Button Content="Random List Change" Click="Button_Click" HorizontalAlignment="Left" Grid.Row="2"/>
<l:HeaderFooterWrapPanel x:Name="headerFooterWrapPanel1" ItemsSource="{Binding Items}"
Header="Header Item" Footer="Footer Item" Grid.Row="3">
<l:HeaderFooterWrapPanel.Resources>
<DataTemplate DataType="{x:Type s:String}">
<Border BorderBrush="Black" BorderThickness="1">
<TextBlock Text="{Binding}" FontSize="16"/>
</Border>
</DataTemplate>
</l:HeaderFooterWrapPanel.Resources>
</l:HeaderFooterWrapPanel>
</Grid>
</Window>
For the purpose of illustration, I've set the Window.DataContext property to the Window object itself. This isn't normally a good idea — it's better to have a proper view model to use as the data context — but for a simple program like this, it's fine. Similarly, the Header and Footer properties would normally be bound to some view model property instead of just tying one framework element's property to another.
C#:
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
Items.Add("Item #1");
Items.Add("Item #2");
Items.Add("Item #3");
}
private static readonly Random _random = new Random();
private void Button_Click(object sender, RoutedEventArgs e)
{
switch (Items.Count > 0 ? _random.Next(2) : 0)
{
case 0: // add
Items.Insert(_random.Next(Items.Count + 1), $"Item #{_random.Next()}");
break;
case 1: // remove
Items.RemoveAt(_random.Next(Items.Count));
break;
}
}
}
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>
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
I am working on a Windows Store app in which I have a user control as a data template inside flipview.
User Control: (ImagePage.xaml)
<UserControl
x:Name="userControl"
x:Class="MWC_online.Classes.ImagePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MWC_online.Classes"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="768"
d:DesignWidth="1366">
<Grid Background="#FFF0F0F0" Margin="4,0">
...
<Image Source="{Binding Img}" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top" />
<StackPanel x:Name="stackPanel" HorizontalAlignment="Left" Margin="984,83,0,0" Width="325">
<Grid Background="{Binding Colour}">
<TextBlock Margin="30,30,30,15" Text="{Binding TextContent1}" FontWeight="Light" TextWrapping="Wrap" Foreground="#FF00ABE8" FontSize="29" />
</Grid>
<Grid Background="{Binding Colour}">
<TextBlock Margin="30,10,30,30" Text="{Binding TextContent2}" TextWrapping="Wrap" Foreground="#FF606060" FontSize="17" />
</Grid>
</StackPanel>
</Grid>
</UserControl>
User Control Class: (ImagePage.xaml.cs)
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){
StackPanel stackPanel = (StackPanel)d;
stackPanel.Visibility = Visibility.Collapsed;
}
public string TextContent1
{
get { return (string)GetValue(TextContent1Property); }
set { SetValue(TextContent1Property, value); }
}
public static readonly DependencyProperty TextContent1Property =
DependencyProperty.Register("TextContent1", typeof(string), typeof(ImagePage), new PropertyMetadata("", new PropertyChangedCallback(OnTextContent1Changed)));
private static void OnTextContent1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// what I want to do is if TextContent1 or TextContent2 has no value
// turn the stackpanel visibility to collapsed
StackPanel stackPanel = (StackPanel)d;
stackPanel.Visibility = Visibility.Collapsed;
}
Everything is working fine EXCEPT the OnTextContent1Changed is not firing! so I dont know if this is the right way of doing things but basically I just want to switch an UI element within the user control ON or OFF depending on the data binding that is being fed into it.
The TextBlock doesn't have a DataContext to find the DependencyProperty on. If you give your Grid a name in ImagePage.xaml:
<Grid Background="#FFF0F0F0" Margin="4,0" x:Name="MyGrid">
Then you can set its DataContext in the ImagePage constructor in ImagePage.xaml.cs:
public ImagePage()
{
InitializeComponent();
MyGrid.DataContext = this;
}
Which tells the Grid (and its decendents) to look for Dependency Properties on the ImagePage class. With this, the Dependency Property should get bound correctly. Another problem, though, is that you're telling the DependencyProperty that it is on an ImagePage with typeof(ImagePage), but then casting it to a StackPanel, which will fail every time:
StackPanel stackPanel = (StackPanel)d; // Throws System.InvalidCastException
You could fix this by giving a name to the StackPanel and referencing it directly in your .cs file.
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.