This is my first question here on SO...I have been a ready for a long time and never needed to ask for help because I usually find what I need, but I am having a hard time with this one...
I am working on a tools suite in WPF. I created a few User Controls as follow:
LabelTextBox (Label on the left and TextBox on the right)
LabelTextBoxToggle (LabelTextBox on the left and Checkbox on the right)
LabelTextBoxBrowseFile (LabelTextBox on the left and Browse File Button on the right)
I use Dependency Properties to bind all the properties I need and they all work fine. The problem I ran into recently is getting ValidationRules to work correctly on the base TextBox I use in LabelTextBox when those rules are applied to the LabelTextBoxToggle and LabelTextBoxBrowseFile UserControls, since I have to bind 2 levels down in order to update controls in LabelTextBox. I can get the Validation Rule to run, but I can't get the TextBox control to update its background color accordingly when errors are found, like I do when LabelTextBox isn't nested within another User Control.
So, here's my code below:
Style used for TextBox:
<!-- TextBox Default Style, Supports Validation Rules -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="{StaticResource TextBoxBGDefault}" />
<Style.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="{StaticResource TextBoxBGHasFocus}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{StaticResource TextBoxBGHasFocus}" />
</Trigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError)}" Value="true">
<Setter Property="Background" Value="{StaticResource TextBoxBGHasError}" />
<Setter Property="BorderBrush" Value="Firebrick" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="ToolTipService.InitialShowDelay" Value="2" />
<Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent}" />
</DataTrigger>
</Style.Triggers>
</Style>
LabelTextBox.xaml:
<Grid x:Name="LayoutRoot" DataContext="{Binding ElementName=ControlRoot, Mode=OneWay, ValidatesOnDataErrors=True}">
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label
x:Name="NameLabel"
Width="{Binding Path=LabelWidth, Converter={StaticResource WidthToAutoConverter}}"
Margin="0"
HorizontalAlignment="{Binding Path=HorizontalContentAlignment}"
HorizontalContentAlignment="{Binding Path=LabelHAlign, Converter={StaticResource valueToStringConverter}}"
VerticalContentAlignment="Center"
Content="{Binding Path=LabelContent}"
Padding="10,2,5,2" />
<TextBox
x:Name="ValueTextBox"
Grid.Column="1"
KeyDown="TextBox_KeyDown_Enter"
Padding="5,0"
Text="{Binding TextBoxContent, Mode=TwoWay}"
TextChanged="TextBox_TextChanged" VerticalContentAlignment="Center" Height="22" VerticalAlignment="Center" />
<TextBlock
x:Name="ErrorMsgTextBlock"
Grid.Row="1"
Grid.Column="1"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Style="{DynamicResource ValidationErrorLabel}"
Text="{Binding Path=(Validation.Errors)[0].ErrorContent, ElementName=ControlRoot}"
Visibility="{Binding Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilityConverter}, ElementName=ControlRoot, Mode=OneWay}" TextWrapping="Wrap" />
</Grid>
LabelTextBoxBaseClass:
#region TextBox Dependency Properties
public string TextBoxContent
{
get { return (string)GetValue( TextBoxContentProperty ); }
set { SetValue( TextBoxContentProperty, value ); }
}
public static readonly DependencyProperty TextBoxContentProperty =
DependencyProperty.Register( "TextBoxContent"
, typeof( string )
, typeof( LabelTextBoxBaseClass ), new PropertyMetadata( "" )
);
LabelTextBoxToggle.xaml:
<!-- This is the nested UserControl -->
<local:LabelTextBox
x:Name="LTBControl"
Margin="0"
VerticalContentAlignment="Center"
IsEnabled="{Binding Path=IsChecked, ElementName=ToggleCheckBox}"
LabelContent="{Binding Path=LabelContent}"
LabelHAlign="{Binding Path=LabelHAlign}"
LabelWidth="{Binding Path=LabelWidth}"
RaiseEnterKeyDownEvent="{Binding RaiseEnterKeyDownEvent, Mode=TwoWay}"
RaiseTextChangedEvent="{Binding RaiseTextChangedEvent, Mode=TwoWay}"
TextBoxContent="{Binding Path=TextBoxContent, Mode=TwoWay}" />
<CheckBox
x:Name="ToggleCheckBox"
Grid.Column="1"
Margin="5,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Click="ToggleCheckBox_Click"
IsChecked="{Binding CheckBoxChecked, Mode=TwoWay}" />
MaterialBuilder.xaml:
<UserControl.Resources>
<BindingGroup x:Key="SRBindingGroup" Name="PropertiesBindingGroup">
<BindingGroup.ValidationRules>
<local:AddMaterialRule ValidationStep="ConvertedProposedValue" />
</BindingGroup.ValidationRules>
</BindingGroup>
<srvalidators:StringNullOrEmptyValidationRule x:Key="stringNullOrEmptyValidationRule" ErrorMessage="Custom Dir cannot be null!" />
<srconverters:ListToStringConverter x:Key="ListToStringConverter" />
<srconverters:ListToStringConverter x:Key="listToStringConverter" />
<sys:String x:Key="newLine">\n</sys:String>
</UserControl.Resources>
<StackPanel x:Name="spSetup">
<!-- This contains a nested UserControl (LabelTextBox), and I can't get its TextBox background to change color, I just get the red border around the whole control on Validation Errors. -->
<srcontrols:LabelTextBoxBrowseFile
x:Name="ltbMaterialBlueprint"
Height="Auto"
Margin="0,5"
LabelContent="Material Blueprint:"
LabelWidth="120"
LostFocus="ltbMaterialBlueprint_UpdateUI"
OnButtonClick="ltbMaterialBlueprint_UpdateUI"
OnTextBoxEnterKeyDown="ltbMaterialBlueprint_UpdateUI"
TextBoxContent="{Binding MaterialBlueprintFilePath, Mode=TwoWay}">
<srcontrols:LabelTextBoxBrowseFile.TextBoxContent>
<Binding
Mode="TwoWay"
Path="CustomDirName"
UpdateSourceTrigger="PropertyChanged"
ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<srvalidators:StringNullOrEmptyValidationRule ErrorMessage="Custom Dir cannot be empty!" />
</Binding.ValidationRules>
</Binding>
</srcontrols:LabelTextBoxBrowseFile.TextBoxContent>
</srcontrols:LabelTextBoxBrowseFile>
<!-- Here I use the base LabelTextBox control by itself and everything works as intended. The TextBox's background color changes to red on Validation Errors. -->
<srcontrols:LabelTextBox
x:Name="ltbMaterialName"
Margin="0,5,10,5"
LabelContent="Name:"
LabelWidth="60"
OnTextBoxTextChange="ltbMaterialName_Validate"
RaiseEnterKeyDownEvent="True"
RaiseTextChangedEvent="True">
<!-- Set-up the TextBox Content to use the ValidationRule by passing this GroupBox's BindingGroup resource as a parameter -->
<srcontrols:LabelTextBox.TextBoxContent>
<Binding
Mode="TwoWay"
Path="MaterialName"
UpdateSourceTrigger="Explicit"
ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<local:AddMaterialRule
BGroup="{StaticResource SRBindingGroup}"
CheckForDuplicates="True"
CheckForEmptyName="True"
IsMaterialName="True"
ValidationStep="ConvertedProposedValue" />
</Binding.ValidationRules>
</Binding>
</srcontrols:LabelTextBox.TextBoxContent>
</srcontrols:LabelTextBox>
</StackPanel>
I know it's probably a DataContext issue, but unlike the other controls and dependency properties, I cannot figure out how to make the base UserControl ui elements update their look when Validation Errors are found. Here's some images of what I mean:
Working TextBox (LabelTextBox control used here):
Working TextBox Example
Broken TextBox (LabelTextBoxToggle control used here, with nested LabelTextBox):
Broken TextBox (nested in UserControl)
Any help or suggestion is very welcomed of course! Thanks for your time!
Your problem is similar to mine. I've also created custom control containing text block (as label) and text box (as input). The goal is to have universal control for data input with simple label. The problem was validation. I've also managed easily to bind and validate data, but displaying errors with template on specified textbox that was inside my control... that was the issue and if I understand correctly you have the same problem. So my solution is:
<UserControl x:Class="CapMachina.Common.Controls.FormField_UC" x:Name="FormFieldCtrl"
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:CapMachina.Common.Controls"
xmlns:Converters="clr-namespace:CapMachina.Common.Converters"
xmlns:metro="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Converters:ConditionalValueConverter x:Key="conditionalValueConverter" />
<Converters:NullableObjectToVisibilityConverter x:Key="nullableObjectToVisibilityConverter" />
</UserControl.Resources>
<StackPanel>
<TextBlock FontWeight="Bold" Text="{Binding Header, ElementName=FormFieldCtrl}" Margin="1" />
<TextBox x:Name="MainTxtBx" metro:TextBoxHelper.Watermark="{Binding WaterMarkText, ElementName=FormFieldCtrl}" TextWrapping="Wrap"
Text="{Binding Text, ElementName=FormFieldCtrl, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Margin="1" IsReadOnly="{Binding IsReadOnly, ElementName=FormFieldCtrl}" TextChanged="MainTxtBx_TextChanged" Loaded="MainTxtBx_Loaded">
<TextBox.Style>
<MultiBinding Converter="{StaticResource conditionalValueConverter}">
<Binding Path="IsReadOnly" ElementName="FormFieldCtrl" />
<Binding Path="ReadOnlyStyle" ElementName="FormFieldCtrl" />
<Binding Path="DefaultStyle" ElementName="FormFieldCtrl" />
</MultiBinding>
</TextBox.Style>
</TextBox>
</StackPanel>
</UserControl>
And code behind:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace CapMachina.Common.Controls
{
public partial class FormField_UC : UserControl
{
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(FormField_UC));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(FormField_UC));
public string WaterMarkText
{
get { return (string)GetValue(WaterMarkTextProperty); }
set { SetValue(WaterMarkTextProperty, value); }
}
public static readonly DependencyProperty WaterMarkTextProperty =
DependencyProperty.Register("WaterMarkText", typeof(string), typeof(FormField_UC));
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(FormField_UC), new PropertyMetadata(true));
public Style ReadOnlyStyle { get; set; }
public Style DefaultStyle { get; set; }
public FormField_UC()
{
ReadOnlyStyle = Application.Current.FindResource("ReadOnlyTextBox") as Style;
DefaultStyle = Application.Current.FindResource("DefaultTextBox") as Style;
InitializeComponent();
}
private void MainTxtBx_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(MainTxtBx.Text) && IsReadOnly)
Visibility = Visibility.Collapsed;
else
Visibility = Visibility.Visible;
}
private void MainTxtBx_Loaded(object sender, RoutedEventArgs e)
{
BindingExpression mainTxtBxBinding = BindingOperations.GetBindingExpression(MainTxtBx, TextBox.TextProperty);
BindingExpression textBinding = BindingOperations.GetBindingExpression(this, TextProperty);
if (textBinding != null && mainTxtBxBinding != null && textBinding.ParentBinding != null && textBinding.ParentBinding.ValidationRules.Count > 0 && mainTxtBxBinding.ParentBinding.ValidationRules.Count < 1)
{
foreach (ValidationRule vRule in textBinding.ParentBinding.ValidationRules)
mainTxtBxBinding.ParentBinding.ValidationRules.Add(vRule);
}
}
}
}
Usage:
<Controls:FormField_UC Header="First name" IsReadOnly="False" HorizontalAlignment="Left" VerticalAlignment="Top">
<Controls:FormField_UC.Text>
<Binding Path="Person.FirstName" Mode="TwoWay">
<Binding.ValidationRules>
<VDRules:NamesValidationRule InventoryPattern="{StaticResource NamesRegex}">
<VDRules:NamesValidationRule.Attributes>
<Validation:ValidationAttributes IsRequired="True" />
</VDRules:NamesValidationRule.Attributes>
</VDRules:NamesValidationRule>
</Binding.ValidationRules>
</Binding>
</Controls:FormField_UC.Text>
</Controls:FormField_UC>
What i did was copy validation rules to nested text box after all bindings were created. You cannot modify binding after use, but you can add validation rules to it :)
It is very important to set certain properties inside custom control like:
<UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True>
cos you cannot set them afterwards. So setting them in usage line is not needed.
Related
The main idea is to have a button with default icon "yes.png" and text in it "Accept", but have a possibilty to change these two properties using only XAML(at the designing process, without compiling).
Current XAML window which has an area at the bottom with only two buttons:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<ResourceDictionary>
<Style x:Key="tb1" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="1" BorderBrush="#000" Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source="Files/Icons/no.png" Margin="10,0,0,0" Height="16" Width="16"></Image>
<TextBlock Grid.Column="1" Margin="10" VerticalAlignment="Center">Cancel</TextBlock>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Margin" Value="0,10,10,10"></Setter>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Border BorderThickness="0, 1, 0, 0" BorderBrush="#e7e7e7" HorizontalAlignment="Stretch" Padding="0,0,0,0" VerticalAlignment="Bottom" Height="61">
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="b_Accept" Style="{StaticResource tb1}"></Button> <!-- How to change an icon to "yes.png" and TextBlock's content to "Accept"? -->
<Button x:Name="b_Cancel" Style="{StaticResource tb1}"></Button>
</StackPanel>
</Border>
</Grid>
</Window>
Result:
Please,
How is it possible to change second button's icon to "no.png"(Source property) and TextBlock's text(Content) to "Cancel"(only using XAML and not User Control)?
What would be the very right way(the easiest?)? For example, in this post we might use DataTemplate, but might be that's not that we want to because DataTemplate changes the whole element, while we need only one property.
Although, I am right that there are only dependency property(C#) available for that purpose which expects compiling?
Thank you
You can create your custom Button class or an Attached Property to extend the Button:
public class IconControl : DependencyObject
{
#region IconUri attached property
public static readonly DependencyProperty IconUriProperty = DependencyProperty.RegisterAttached(
"IconUri", typeof(ImageSource), typeof(IconControl), new PropertyMetadata(default(ImageSource)));
public static void SetIconUri([NotNull] DependencyObject attachingElement, ImageSource value)
{
attachingElement.SetValue(IconControl.IconUriProperty, value);
}
public static ImageSource GetIconUri([NotNull] DependencyObject attachingElement) => (ImageSource) attachingElement.GetValue(IconControl.IconUriProperty);
#endregion
#region Label attached property
public static readonly DependencyProperty LabelProperty = DependencyProperty.RegisterAttached(
"Label", typeof(String), typeof(IconControl), new PropertyMetadata(default(String)));
public static void SetLabel([NotNull] DependencyObject attachingElement, String value)
{
attachingElement.SetValue(IconControl.LabelProperty, value);
}
public static String GetLabel([NotNull] DependencyObject attachingElement) => (String) attachingElement.GetValue(IconControl.LabelProperty);
#endregion
}
Modified Style for the Button:
<Style x:Key="IconButtonStyle"
TargetType="{x:Type Button}">
<!-- Set the default values -->
<Setter Property="IconControl.IconUri" Value="/Files/Icons/no.png"/>
<Setter Property="IconControl.Label" Value="Cancel"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border BorderThickness="1"
BorderBrush="#000"
Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Source="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(IconControl.IconUri)}"
Margin="10,0,0,0"
Height="16"
Width="16" />
<TextBlock Grid.Column="1"
Margin="10"
VerticalAlignment="Center"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(IconControl.Label)}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Margin"
Value="0,10,10,10"></Setter>
</Style>
Usage:
<!-- Override the default content -->
<Button Style="{StaticResource IconButtonStyle}"
IconControl.IconUri="/Files/Icons/yes.png"
IconControl.Label="Accept" />
I have custom ContentControl
public class FilteringColumnHeader : ContentControl
{
public static readonly DependencyProperty TextFieldProperty =
DependencyProperty.Register("TextField", typeof(string), typeof(FilteringColumnHeader), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string TextField
{
get
{
return (string)GetValue(TextFieldProperty);
}
set { SetValue(TextFieldProperty, value); }
}
}
With this template style
<Style TargetType="{x:Type c:FilteringColumnHeader}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type c:FilteringColumnHeader}">
<DockPanel>
<ContentPresenter DockPanel.Dock="Top" Content="{TemplateBinding Content}" />
<TextBox Text="{TemplateBinding TextField}"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And this is how I use it in DataGrid in XAML
<DataGridTextColumn x:Name="NameColumn" Header="Name" Binding="{Binding Name}" Width="*" MinWidth="50">
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<c:FilteringColumnHeader Content="{Binding }" Width="{Binding ActualWidth, ElementName=NameColumn}" TextField="{Binding DataContext.NameFilter, RelativeSource={RelativeSource AncestorType={x:Type local:GeneratorsListView}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
And GeneratorsListView
private string nameFilter = "rec";
public string NameFilter
{
get { return nameFilter; }
set
{
nameFilter = value;
}
}
My problem is, the Text binding works only OneWay. When I run the code TextBox will be filled with "rec" and when I change the NameFilter, the TextBox also changes. But when I type something in that box, nothing happens (setter of NameFilter is not being invoked at all). As you can see I've tried to set mode to TwoWay everywhere I could, still nothing. When I pleace regular TextBox inside DataTemplate and set the exact same Text binding, it is working.
{TemplateBinding} is an optimized version of a binding with a mode of OneWay so if you want the property to get updated you should use an ordinary binding with the RelativeSource set to TemplatedParent:
<ControlTemplate TargetType="{x:Type c:FilteringColumnHeader}">
<DockPanel>
<ContentPresenter DockPanel.Dock="Top" Content="{TemplateBinding Content}" />
<TextBox Text="{Binding TextField, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource TemplatedParent}}"/>
</DockPanel>
</ControlTemplate>
I am working in Visual Studio 2013 in WPF (C#) and I have the following code to create an expander from my xml. It is currently working perfectly right now but I want to include an expand all and collapse all buttons. I have looked all over and can't seem to find a solution.
Here is where the expander is created. I know I just have to iterate through a list of items and change the Property="IsExpanded" to Value="True" to create an expand all.
<DataTemplate x:Key="dtListTemplate" >
<StackPanel>
<Expander LostFocus="CollapseExpander" ExpandDirection="Down" Width="Auto">
<Expander.Style>
<Style TargetType="Expander">
<Setter Property="IsExpanded" Value="False" />
<Setter Property="Header" Value="{Binding XPath=#Name}" />
<Setter Property="FontWeight" Value="Bold"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsExpanded,RelativeSource={RelativeSource Self}}" Value="True">
</DataTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
<ListBox Name="itemsList"
ItemsSource="{Binding XPath=UpgradeAction}"
ItemTemplate="{StaticResource dtListItemTemplate}"
SelectionChanged="listItems_SelectionChanged"
Style="{StaticResource styleListBoxUpgradeAction}"
ItemContainerStyle="{StaticResource styleListBoxItemUpgradeAction}">
</ListBox>
</Expander>
</StackPanel>
</DataTemplate>
Here's the code that calls the DataTemplate which has information from an Xml.
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Border Grid.Column="0" Grid.Row="0" Width="790" Height="40" Padding="5" Background="#4E87D4">
<Label VerticalAlignment="Center" FontSize="16" FontWeight="Bold" Foreground="White">Test</Label>
</Border>
<Button Name="ExpandBttn" Width="100" Height="40" FontSize="16" FontWeight="Bold" Content="Expand All" DataContext="{Binding}" Click="Expand_Button_Click"/>
<Button Name="ColapseBttn" Width="100" Height="40" FontSize="16" FontWeight="Bold" Content="Colapse All" DataContext="{Binding}" Click="Collapse_Button_Click"/>
</StackPanel>
<ListView Name="listItems" Grid.Column="0" Grid.Row="1" Background="Wheat"
ItemsSource="{Binding Source={StaticResource xmldpUpgradeActions}, XPath=ActionGroup}"
ItemTemplate="{StaticResource dtListTemplateRichards}"
SelectionChanged="listItems_SelectionChanged">
</ListView>
</StackPanel>
Here's what I tried in the .cs file for the expand all portion.
private void Expand_Button_Click(object sender, RoutedEventArgs e)
{
foreach(var item in listItems.Items)
{
var listBoxItem = listItems.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem;
var itemExpander = (Expander)GetExpander(listBoxItem);
if (itemExpander != null)
itemExpander.IsExpanded = true;
}
}
private static DependencyObject GetExpander(DependencyObject container)
{
if (container is Expander) return container;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(container); i++)
{
var child = VisualTreeHelper.GetChild(container, i);
var result = GetExpander(child);
if (result != null)
{
return result;
}
}
return null;
}
Any help is greatly appreciated!
Is xmldpUpgradeActions a CollectionViewSource?
Whatever class is in the collection, it should implement INotifyPropertyChanged.
Give it an IsExpanded property that raises PropertyChanged in its setter when its value changes, and bind that to Expander.IsExpanded in the template:
<Expander
IsExpanded="{Binding IsExpanded}"
LostFocus="CollapseExpander"
ExpandDirection="Down"
Width="Auto">
Write a command that loops through all the items in the collection and sets item.IsExpanded = false; on each one.
As topic mentioned.I want to use only one popup for all button in my application.I don't know how to get what I want.
Here is what my window looks like:
Info 1:
Info 2:
You can see popup appear on wrong position.I know I can position a popup by setting the PlacementTarget.But each Popup has a different value for the placement property.That is the problem.I'm looking another way to do it.
Here is a popup for option 1:
<StackPanel Orientation="Horizontal">
<!--Option 1: text and button-->
<TextBlock Text="Option 1"
Margin="10"
VerticalAlignment="Center" />
<Popup x:Name="popInfo"
PlacementTarget="{Binding ElementName=btnInfoOption1}"
IsOpen="{Binding IsShowInfo1}">
<ContentControl Style="{StaticResource ContentInfoStyle}">
<TextBlock Text="{Binding InfoContent}"
TextWrapping="Wrap"
Foreground="White"
Width="340"
Padding="10"
Margin="30,0,30,5"
FontSize="15" />
</ContentControl>
</Popup>
<Button x:Name="btnInfoOption1"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>
popup for option 2:
<StackPanel Orientation="Horizontal">
<!--Option 2: text and button-->
<TextBlock Text="Option 2"
Margin="10"
VerticalAlignment="Center" />
<Button x:Name="btnOption2"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>
ContentControl Style:
<Style TargetType="{x:Type ContentControl}"
x:Key="ContentInfoStyle">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Border Background="Green"
CornerRadius="3"
Padding="10,0,12,10">
<StackPanel>
<Button HorizontalAlignment="Right"
Tag="{StaticResource ic_gclear}"
Style="{StaticResource btnIcons}"
Background="White"
Margin="10,5,12,5"
Command="{Binding DataContext.CmdCloseInfo}"
Height="24" />
<ContentPresenter x:Name="content"
TextBlock.FontSize="14"
TextBlock.Foreground="White"
TextBlock.FontFamily="Arial"
Content="{TemplateBinding ContentControl.Content}" />
</StackPanel>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Button icon style:
<Style TargetType="Button"
x:Key="btnIcons">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="brd" Background="Transparent"
SnapsToDevicePixels="True">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Path Stretch="Uniform" VerticalAlignment="Center"
Fill="{TemplateBinding Background}"
Data="{TemplateBinding Tag}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
ViewModel.cs:
the content of popup:
private string _InfoContent;
public string InfoContent
{
get { return _InfoContent; }
set
{
if (value != _InfoContent)
{
_InfoContent = value;
OnRaise("InfoContent");
}
}
}
show the popup for option2 and option1:
private bool _IsShowInfo2;
public bool IsShowInfo2
{
get { return _IsShowInfo2; }
set
{
if (value != _IsShowInfo2)
{
_IsShowInfo2 = value;
OnRaise("IsShowInfo2");
}
}
}
//show the popup for option1
private bool _IsShowInfo1;
public bool IsShowInfo1
{
get { return _IsShowInfo1; }
set
{
if (value != _IsShowInfo1)
{
_IsShowInfo1 = value;
OnRaise("IsShowInfo1");
}
}
}
the command for button:
private ICommand _CmdShowInfo;
public ICommand CmdShowInfo
{
get
{
_CmdShowInfo = _CmdShowInfo ?? new RelayCommand(x => this.ShowInfo(true, 1), () => true);
return _CmdShowInfo;
}
}
private ICommand _CmdShowInfo2;
public ICommand CmdShowInfo2
{
get
{
_CmdShowInfo2 = _CmdShowInfo2 ?? new RelayCommand(x => this.ShowInfo(true, 0), () => true);
return _CmdShowInfo2;
}
}
private void ShowInfo(bool show = true, byte option = 0)
{
if (option == 0)
{
this.InfoContent = "Option 1...";
}
else if (option == 1)
{
this.InfoContent = "Option 2...";
}
this.IsShowInfo1 = show;
}
My initial thought was to do this with a styled HeaderedContentControl, but then you've got the icon fill color and the icon data, and I'd have had to add attached properties for those. Once you go there, you may as well just write a custom control.
The dependency properties of IconPopupButton can be bound like any dependency property:
<hec:IconPopupButton
IsOpen="{Binding IsShowInfo1}"
IconFill="YellowGreen"
Content="Another Test Popup"
IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
/>
If you want to parameterize the Style applied to the ContentControl in the Popup, add another dependency property. You'll need to give that some thought, though, because you need that ToggleButton to be bound to IsOpen on the templated parent, one way or another. Perhaps you could bind it to the viewmodel property that's bound to the popup button's IsOpen. There's always a way.
So here's that. With snippets to create dependency properties, this is pretty much just a fill-in-the-blanks exercise. Much less to it than meets the eye.
IconPopupButton.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace HollowEarth.Controls
{
public class IconPopupButton : ContentControl
{
static IconPopupButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(IconPopupButton), new FrameworkPropertyMetadata(typeof(IconPopupButton)));
}
#region IconData Property
public Geometry IconData
{
get { return (Geometry)GetValue(IconDataProperty); }
set { SetValue(IconDataProperty, value); }
}
public static readonly DependencyProperty IconDataProperty =
DependencyProperty.Register("IconData", typeof(Geometry), typeof(IconPopupButton),
new PropertyMetadata(null));
#endregion IconData Property
#region IconFill Property
public Brush IconFill
{
get { return (Brush)GetValue(IconFillProperty); }
set { SetValue(IconFillProperty, value); }
}
public static readonly DependencyProperty IconFillProperty =
DependencyProperty.Register("IconFill", typeof(Brush), typeof(IconPopupButton),
new PropertyMetadata(SystemColors.ControlTextBrush));
#endregion IconFill Property
#region IsOpen Property
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(IconPopupButton),
new PropertyMetadata(false));
#endregion IsOpen Property
#region StaysOpen Property
public bool StaysOpen
{
get { return (bool)GetValue(StaysOpenProperty); }
set { SetValue(StaysOpenProperty, value); }
}
public static readonly DependencyProperty StaysOpenProperty =
DependencyProperty.Register("StaysOpen", typeof(bool), typeof(IconPopupButton),
new PropertyMetadata(false));
#endregion StaysOpen Property
#region Placement Property
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(IconPopupButton),
new PropertyMetadata(PlacementMode.Right));
#endregion Placement Property
}
}
Themes\Shared.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
>
<Geometry x:Key="ic_gclear">M56,4 52,0 28,24 4,0 0,4 24,28 0,52 4,56 28,32 52,56 56,52 32,28Z</Geometry>
<Geometry x:Key="ic_ginfo">M31,0C13.879,0,0,13.879,0,31s13.879,31,31,31s31-13.879,31-31S48.121,0,31,0z M34,46h-6V27.969h6V46z M34,21.969h-6V16h6V21.969z</Geometry>
<Style TargetType="ButtonBase" x:Key="btnIcons">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Border x:Name="brd" Background="Transparent" SnapsToDevicePixels="True">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Path
x:Name="Path"
Stretch="Uniform"
VerticalAlignment="Center"
Fill="{TemplateBinding Background}"
Data="{TemplateBinding Tag}"
/>
<TextBlock
x:Name="MissingIconData"
Visibility="Collapsed"
Text="?"
FontWeight="Bold"
FontSize="30"
ToolTip="IconData (Tag) not set"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="{x:Null}">
<Setter TargetName="MissingIconData" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Themes\Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
xmlns:hec="clr-namespace:HollowEarth.Controls"
>
<ResourceDictionary.MergedDictionaries>
<!-- Change HeaderedPopupTest to the name of your own assembly -->
<ResourceDictionary Source="/HeaderedPopupTest;component/Themes/Shared.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="hec:IconPopupButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="hec:IconPopupButton">
<Grid x:Name="Grid">
<ToggleButton
x:Name="OpenButton"
Style="{StaticResource btnIcons}"
Background="{TemplateBinding IconFill}"
Tag="{TemplateBinding IconData}"
IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ToolTip="{TemplateBinding ToolTip}"
/>
<Popup
x:Name="Popup"
StaysOpen="{Binding StaysOpen, RelativeSource={RelativeSource TemplatedParent}}"
IsOpen="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
PlacementTarget="{Binding ElementName=ToggleButton}"
Placement="{TemplateBinding Placement}"
>
<Border
Background="Green"
CornerRadius="3"
Padding="10,0,12,10">
<StackPanel>
<ToggleButton
HorizontalAlignment="Right"
Tag="{StaticResource ic_gclear}"
Style="{StaticResource btnIcons}"
Background="White"
Margin="10,5,12,5"
IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Height="24"
/>
<ContentPresenter
x:Name="content"
TextBlock.FontSize="14"
TextBlock.Foreground="White"
TextBlock.FontFamily="Arial"
Content="{TemplateBinding Content}"
/>
</StackPanel>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--
I don't understand this: If I use the templated parent's IsOpen,
the effect is as if it were never true.
-->
<Condition SourceName="Popup" Property="IsOpen" Value="True" />
<Condition Property="StaysOpen" Value="False" />
</MultiTrigger.Conditions>
<!--
If StaysOpen is false and the button is enabled while the popup is open,
then clicking on it will cause the popup to flicker rather than close.
-->
<Setter TargetName="OpenButton" Property="IsEnabled" Value="False" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
MainWindow.xaml example usage:
<Window
x:Class="HeaderedPopupTest.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:HeaderedPopupTest"
xmlns:hec="clr-namespace:HollowEarth.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes\Shared.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style
x:Key="InfoPopupButton"
TargetType="hec:IconPopupButton"
BasedOn="{StaticResource {x:Type hec:IconPopupButton}}"
>
<Setter Property="IconFill" Value="DeepSkyBlue" />
<Setter Property="IconData" Value="{StaticResource ic_ginfo}" />
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid>
<StackPanel
Orientation="Vertical"
HorizontalAlignment="Left"
>
<hec:IconPopupButton
Style="{StaticResource InfoPopupButton}"
Content="This is a test popup"
ToolTip="Test Popup Tooltip"
/>
<hec:IconPopupButton
IconFill="YellowGreen"
Content="Another Test Popup"
IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
/>
<hec:IconPopupButton
IconFill="DarkRed"
Content="Missing IconData behavior example"
/>
</StackPanel>
</Grid>
</Window>
You'll notice I changed your buttons to ToggleButton. This is for convenience in wiring them up to the IsOpen property: With a ToggleButton, I just bind IsChecked and I'm done. No need for commands. One side effect of that is that if StaysOpen is false, then when the user clicks on the open button for a Popup, the focus change closes the Popup, which unchecks the button, and then the button gets the mouse message. So the button opens the popup again. This is bizarre behavior from the user's perspective, so you add a trigger to disable the button when the popup is open and StaysOpen is false. When StaysOpen is true, focus change doesn't close the Popup, so you want the button to be enabled in that case.
I changed the btnIcons style to target ButtonBase, so it works identically with Button and ToggleButton.
I'm using Visual Studio 2015 and MVVM Light Toolkit to build a WPF app. When the user clicks an employee in a DataGrid, we show the record's details to allow editing. This details area consists of two tabs: Demographics and Tests. The Tests tab displays a ListView of the tests for this person.
Here's the structure:
MainWindow.xaml:
<DataTemplate x:Key="EmployeeSearchTemplate">
<view:EmployeeSearchView />
</DataTemplate>
<ContentControl ContentTemplate="{StaticResource EmployeeSearchTemplate}" />
EmployeeSearchView.xaml:
<UserControl.DataContext>
<viewModel:EmployeeSearchViewModel />
</UserControl.DataContext>
<ContentControl Content="{Binding SelectedEmployee}"
ContentTemplate="{StaticResource EmployeeViewTemplate}" .../>
When the user selects the Tests tab, we search the db and return the tests for this employee, if any.
EmployeeView.xaml:
<DataTemplate x:Key="TestsViewTemplate">
<views:TestsView />
</DataTemplate>
<TabControl SelectedIndex="{Binding SelectedTabIndex}">
<TabItem>
<!-- Demographic details of record here -->
</TabItem>
<TabItem>
<!-- Employee test info here. When user selects this tab, search db
and return tests for this employee, if any -->
<ContentControl Content="{Binding TestsVm}"
ContentTemplate="{StaticResource TestsViewTemplate}" />
</TabItem>
</TabControl>
Here are the constructor and some properties for EmployeeViewModel.cs:
private TestsViewModel _testsVm;
private int _selectedTabIndex;
public EmployeeViewModel ()
{
// Other initialization code...
_selectedTabIndex = 0;
this.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(SelectedTabIndex))
{
// If tab 1 selected, the person is on the Tests tab
// Perform search and populate the TestsVM object's Tests
// by executing the RelayCommand on it
if (SelectedTabIndex.Equals(1))
{
TestsVm = new TestsViewModel
{
SelectedEmployeeId = EmployeeId
};
TestsVm.SearchTestsRelayCommand.Execute(null);
}
}
};
}
public TestsViewModel TestsVm
{
get { return _testsVm; }
set
{
if (Equals(value, _testsVm)) return;
_testsVm = value;
RaisePropertyChanged();
}
}
public int SelectedTabIndex
{
get { return _selectedTabIndex; }
set
{
if (value == _selectedTabIndex) return;
_selectedTabIndex = value;
RaisePropertyChanged();
}
}
Here's the ListView in TestsView.xaml:
<ListView ItemsSource="{Binding Tests}"
Visibility="{Binding HasTests,
Converter={helpers:BooleanToVisibilityConverter WhenTrue=Visible,
WhenFalse=Hidden}}">
<ListView.View>
<GridView>
<!-- GridView columns here -->
</GridView>
</ListView.View>
</ListView>
Here's code from TestsViewModel.cs:
private ObservableCollection<TestViewModel> _tests;
private int _selectedEmployeeId;
private bool _hasTests;
public TestsViewModel()
{
SearchTestsRelayCommand = new RelayCommand(CallSearchTestsAsync);
this.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(Tests))
{
HasTests = !Tests.Count.Equals(0);
}
};
}
public RelayCommand SearchTestsRelayCommand { get; private set; }
private async void CallSearchTestsAsync()
{
await SearchTestsAsync(SelectedEmployeeId);
}
private async Task SearchTestsAsync(int employeeId)
{
ITestDataService dataService = new TestDataService();
try
{
Tests = await dataService.SearchTestsAsync(employeeId);
}
finally
{
HasTests = !Tests.Count.Equals(0);
}
}
public ObservableCollection<TestViewModel> Tests
{
get { return _tests; }
set
{
if (Equals(value, _tests)) return;
_tests = value;
RaisePropertyChanged();
}
}
public bool HasTests
{
get { return _hasTests; }
set
{
if (value == _hasTests) return;
_hasTests = value;
RaisePropertyChanged();
}
}
public int SelectedEmployeeId
{
get { return _selectedEmployeeId; }
set
{
if (value == _selectedEmployeeId) return;
_selectedEmployeeId = value;
RaisePropertyChanged();
}
}
The HasTests property is not changing and thus not hiding the ListView when it's empty. Note that I also tried the following for the ListView visibility, pointing to its own HasItems to no avail:
Visibility="{Binding HasItems,
RelativeSource={RelativeSource Self},
Converter={helpers:BooleanToVisibilityConverter WhenTrue=Visible,
WhenFalse=Hidden}}"
I've used the same BooleanToVisibilityConverter successfully elsewhere, so it's something with my code. I'm open to your suggestions. Thank you.
Update: Here's the XAML for TestView.xaml:
<UserControl x:Class="DrugComp.Views.TestsView"
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:helpers="clr-namespace:DrugComp.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:viewModel="clr-namespace:DrugComp.ViewModel"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources />
<Grid Width="Auto"
Height="700"
Margin="5,7,5,5"
HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="32" />
<RowDefinition Height="32" />
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Style="{StaticResource Instruction}"
Text="{Binding Instructions}" />
<ListView Grid.Row="1"
Grid.ColumnSpan="2"
Width="Auto"
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Top"
AlternationCount="2"
ItemContainerStyle="{DynamicResource CustomListViewItemStyle}"
ItemsSource="{Binding Tests}"
SelectedItem="{Binding SelectedTest}">
<ListView.Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<Trigger Property="HasItems" Value="False">
<!-- If you want to save the place in the layout, use
Hidden instead of Collapsed -->
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.Style>
<ListView.View>
<GridView>
<GridViewColumn Width="50"
DisplayMemberBinding="{Binding TestId}"
Header="Test ID" />
<GridViewColumn Width="90"
DisplayMemberBinding="{Binding EmployeeId}"
Header="Employee ID" />
<GridViewColumn Width="90"
DisplayMemberBinding="{Binding OrderedDate,
StringFormat='MM/dd/yyyy'}"
Header="Ordered Date" />
<GridViewColumn Width="119"
DisplayMemberBinding="{Binding ValidReasonForTest.Description}"
Header="Reason" />
<GridViewColumn Width="129"
DisplayMemberBinding="{Binding OrderedByWhom}"
Header="Ordered By" />
<GridViewColumn Width="90"
DisplayMemberBinding="{Binding ScheduledDate,
StringFormat='MM/dd/yyyy'}"
Header="Scheduled Date" />
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
As Joe says, you're not getting the notifications. And if you need HasTests for some reason other than hiding this ListView, his answer will help. But that's not the way to do this in a view in XAML.
Update:
A cleaner, simpler way than the answer below.
<!-- In the view's Resources -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
<!-- ... -->
<ListView
Visibility="{Binding HasItems,
RelativeSource={RelativeSource Self},
Converter=BooleanToVisibility}" />
The (second) cleanest, simplest, easiest way is with a trigger in a style, like this:
<ListView>
<ListView.View>
<GridView>
<!-- GridView columns here -->
</GridView>
</ListView.View>
<ListView.Style>
<Style
TargetType="{x:Type ListView}"
BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<Trigger Property="HasItems" Value="False">
<!-- If you want to save the place in the layout, use
Hidden instead of Collapsed -->
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.Style>
</ListView>
Just note that you can't set the Visibility attribute in the XAML like so, because that's a "local" value which will supersede anything the Style does:
<ListView Visibility="Visible" ...>
That's desirable behavior when you want to override styling for a specific instance of a control, but it bites you a lot when you write triggers.
In this specific case I can't imagine any reason you'd do that, but it's a pervasive "gotcha" with styles and triggers in XAML. If you want to set a specific initial value for a property that'll be driven by a trigger, you can do that in a non-triggered Setter in the Style:
<Style
TargetType="{x:Type ListView}"
BasedOn="{StaticResource {x:Type ListView}}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<Trigger Property="HasItems" Value="False">
<!-- If you want to save the place in the layout, use
Hidden instead of Collapsed -->
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
Then it's all one style thing or another, and the trigger will work.
Any descendant of ItemsControl will support the HasItems property: ListBox, ComboBox, MenuItem, you name it. Pretty much any native WPF control that's designed to present a dynamic collection of items (third party control vendors like DevExpress will often ignore this and use their own, often ill-considered, class hierarchy). It's idea for this sort of thing because it's always there, it's very easy to use, and it doesn't matter where the items come from. Whatever you do to put items in that thing, it will hide itself when there aren't any.
Your code to update HasTests:
this.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(Tests))
{
HasTests = !Tests.Count.Equals(0);
}
};
Will only fire when the whole property Tests is changed (i.e. assigned to a new ObservableCollection). Presumably, you're not doing this, instead using Clear, Add or Remove to change the content of Tests.
As a result, your HasTests never get's updated. Try also updating on the Tests.CollectionChange event to catch adding/removing.
Edit: Something like this
this.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(Tests))
{
HasTests = !Tests.Count.Equals(0);
//also update when collection changes:
Tests.CollectionChanged += (o2, e2) =>
{
HasTests = !Tests.Count.Equals(0);
};
}
};