I'm trying to develop a system maintenance screen for my application in which I have several tabs each representing a different maintenance option i.e. maintain system users et cetera. Once a user clicks on edit/new to change a existing record I want to prevent navigating away from the current tab until the user either clicks save or cancel.
After some googling I've found a link http://joshsmithonwpf.wordpress.com/2009/09/04/how-to-prevent-a-tabitem-from-being-selected/ which seemed to solve my problem, or so I thought.
I've tried implementing this, but my event never seems to fire. Below is my XAML.
<TabControl Name="tabControl">
<TabItem Header="Users">
<DockPanel>
<GroupBox Header="Existing Users" Name="groupBox1" DockPanel.Dock="Top" Height="50">
<StackPanel Orientation="Horizontal">
<Label Margin="3,3,0,0">User:</Label>
<ComboBox Width="100" Height="21" Margin="3,3,0,0"></ComboBox>
<Button Width="50" Height="21" Margin="3,3,0,0" Name="btnUsersEdit" Click="btnUsersEdit_Click">Edit</Button>
<Button Width="50" Height="21" Margin="3,3,0,0" Name="btnUsersNew" Click="btnUsersNew_Click">New</Button>
</StackPanel>
</GroupBox>
<GroupBox Header="User Information" Name="groupBox2">
<Button Content="Cancel" Height="21" Name="btnCancel" Width="50" Click="btnCancel_Click" />
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="User Groups">
</TabItem>
</TabControl>
And this is my code
public partial class SystemMaintenanceWindow : Window
{
private enum TEditMode { emEdit, emNew, emBrowse }
private TEditMode _EditMode = TEditMode.emBrowse;
private TEditMode EditMode
{
get { return _EditMode; }
set
{
_EditMode = value;
}
}
public SystemMaintenanceWindow()
{
InitializeComponent();
var view = CollectionViewSource.GetDefaultView(tabControl.Items.SourceCollection);
view.CurrentChanging += this.Items_CurrentChanging;
}
void Items_CurrentChanging(object sender, CurrentChangingEventArgs e)
{
if ((e.IsCancelable) && (EditMode != TEditMode.emBrowse))
{
var item = ((ICollectionView)sender).CurrentItem;
e.Cancel = true;
tabControl.SelectedItem = item;
MessageBox.Show("Please Save or Cancel your work first.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void btnUsersNew_Click(object sender, RoutedEventArgs e)
{
EditMode = TEditMode.emNew;
}
private void btnUsersEdit_Click(object sender, RoutedEventArgs e)
{
EditMode = TEditMode.emEdit;
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
EditMode = TEditMode.emBrowse;
}
}
Apologies now if I'm being stupid, but for the life of me I cannot workout see why my event does not fire when the user clicks between tabs.
Thanks for all your help.
Emlyn
I've come up with a solution which suits my needs. Seems slightly backwards but compared to the other options I found, it seems nice and neat.
Basically I keep a private variable of the current tabIndex and on SelectionChanged event of the TabControl, I'm doing some checks and set the TabControl.SelectedIndex back to this value if the user is not in browse mode.
private void tabControl_SelectionChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
if (e.OriginalSource == tabControl)
{
if (EditMode == TEditMode.emBrowse)
{
_TabItemIndex = tabControl.SelectedIndex;
}
else if (tabControl.SelectedIndex != _TabItemIndex)
{
e.Handled = true;
tabControl.SelectedIndex = _TabItemIndex;
MessageBox.Show("Please Save or Cancel your work first.", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
I was struggling with this too. Just got it working by simply adding the
IsSynchronizedWithCurrentItem="True"
setting to the TabControl. Worked like a charm after that.
According to this post
https://social.msdn.microsoft.com/Forums/vstudio/en-US/d8ac2677-b760-4388-a797-b39db84a7e0f/how-to-cancel-tabcontrolselectionchanged?forum=wpf
this worked for me:
<TabControl>
<TabControl.Resources>
<Style TargetType="TabItem">
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="OnPreviewMouseLeftButtonDown"/>
</Style>
</TabControl.Resources>
<TabItem Header="Tab1"/>
<TabItem Header="Tab2"/>
</TabControl>
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.Source is TabItem) //do not handle clicks on TabItem content but on TabItem itself
{
var vm = this.DataContext as MyViewModel;
if (vm != null)
{
if (!vm.CanLeave())
{
e.Handled = true;
}
}
}
}
Josh is using tab.ItemsSource. You are using tab.Items.SourceCollection. This might be the problem.
Or implement it yourself...
public delegate void PreviewSelectionChangedEventHandler(object p_oSender, PreviewSelectionChangedEventArgs p_eEventArgs);
public class PreviewSelectionChangedEventArgs
{
internal PreviewSelectionChangedEventArgs(IList p_lAddedItems, IList p_lRemovedItems)
{
this.AddedItems = p_lAddedItems;
this.RemovedItems = p_lRemovedItems;
}
public bool Cancel { get; set; }
public IList AddedItems { get; private set; }
public IList RemovedItems { get; private set; }
}
public class TabControl2: TabControl
{
public event PreviewSelectionChangedEventHandler PreviewSelectionChanged;
private int? m_lLastSelectedIndex;
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
// déterminer si on doit annuler la sélection
PreviewSelectionChangedEventArgs eEventArgs = new PreviewSelectionChangedEventArgs(e.AddedItems, e.RemovedItems);
if (m_lLastSelectedIndex.HasValue)
if (PreviewSelectionChanged != null)
PreviewSelectionChanged(this, eEventArgs);
// annuler (ou pas) la sélection
if (eEventArgs.Cancel)
this.SelectedIndex = m_lLastSelectedIndex.Value;
else
m_lLastSelectedIndex = this.SelectedIndex;
}
}
I am starting to grasp XAML so I hope I didn't miss your point. I had the same problem and for me this worked best.
In XAML I define a Style that toggles the ReadOnly State
<!-- prevent the tab from being changed while editing or adding a physician -->
<Style BasedOn="{StaticResource {x:Type TabControl}}"
TargetType="{x:Type TabControl}" x:Key="InactivateTabControl">
<!-- <Setter Property="IsEnabled" Value="True" /> -->
<Style.Triggers>
<DataTrigger Binding="{Binding PhysicianTypeTabAreLocked}" Value="False">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding PhysicianTypeTabAreLocked}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
<TabControl Grid.Row="1" Grid.Column="0" Margin="0, 10, 0, 0"
x:Name="PhysicianTypesTabControl"
SelectedIndex="{Binding PhysicianTypeSelectedTabIndex, Mode=TwoWay}"
Style="{StaticResource InactivateTabControl}">
<!-- rest here ....-->
<TabControl/>
In the viewmodel I have a property
private EditingState _PhysicianEditingState;
public EditingState PhysicianEditingState
{
get { return _PhysicianEditingState; }
set
{
_PhysicianEditingState = value;
PhysicianTypeTabAreLocked = (PhysicianEditingState != EditingState.NotEditing);
NotifyPropertyChanged("PhysicianTypeTabAreLocked");
}
}
Hope this helps.
The thing that finally worked really well for me was to disable the "TabItems" when I didn't want the user to be able to select them, and then to enable them when I did want them too.
<TabControl IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Pages}" SelectedItem="{Binding SelectedPage}" TabStripPlacement="Left">
<TabControl.Resources>
<Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="TabItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}" />
</Style>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<Label Content="{Binding StepName}" Foreground="{DynamicResource TabActiveSelectedFont}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
This allowed my view model (in this case my pages) to be able to determine when they allowed the user to move to the next page. So When they were done with the current page I would "enable" the next page so they could click on it to continue.
You could simply set the TabItems' IsEnabled property to false which prevents navigation/activation but is not disabling the tabs' content, until your condition (save/cancel) is met.
Related
I am using a DataGrid and on a button click I want to be able to change between the CellTemplate and the EditingCellTemplate of the DataGrid Column.
Shows the DataGrid
On load, the DataGrid shows the CellTemplate with the Permission Level.
When the user double clicks inside a Permission Level Cell the template changes to the EditingCellTemplate and a ItemsControl of buttons appears.
Shows the buttons
When the user presses one of these buttons, Admin, Read or Write I want the Permission Level Template to display the CellTemplate just showing the text and not the EditingCellTemplate.
I've thought about using a behaviour but unsure how it would work. Both of my CellTemplates are in a resource dictionary.
CellTemplate which shows the text
<DataTemplate x:Key="PermissionTemplate">
<Border>
<Label Content="{Binding Path=PermissionLevel.Access}" />
</Border>
</DataTemplate>
Editing cell template
<DataTemplate x:Key="EditingPermissionTemplate">
<Border>
<UniformGrid Rows="1" Columns="1">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl, Mode=FindAncestor}, Path=DataContext.AllPermissionLevels}" HorizontalContentAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Button Style="{StaticResource BaseButtonStyle}"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl, Mode=FindAncestor}, Path=DataContext.UpdatePermissionCommand}"
CommandParameter="{Binding}" >
<TextBlock TextWrapping="Wrap" Text="{Binding Path=Access}" />
</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UniformGrid>
</Border>
</DataTemplate>
DataGrid
<DataGrid ItemsSource="{Binding Path=AllUsersModules}" SelectedItem="{Binding Path=SelectedUsersModule}"
Style="{StaticResource BaseDataGridStyle}" SelectionUnit="FullRow">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="{StaticResource WhiteColorBrush}" />
<Setter Property="Foreground" Value="Black" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Orange" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Module" HeaderStyle="{StaticResource DataGridColumnHeaderStyle}" Width="*"
CellTemplate="{StaticResource ModuleTemplate}"/>
<DataGridTemplateColumn Header="Permission Level" HeaderStyle="{StaticResource DataGridColumnHeaderStyle}" Width="*"
CellTemplate="{StaticResource PermissionTemplate}"
CellEditingTemplate="{StaticResource EditingPermissionTemplate}"/>
</DataGrid.Columns>
</DataGrid>
If you want to exit the edit mode when the Button is clicked, you could hook up a Click event handler that calls the CancelEdit() method of the DataGrid. Here is how to do this in a ResourceDictionary.
private void Button_Click(object sender, RoutedEventArgs e)
{
DataGrid dataGrid = FindParent<DataGrid>((Button)sender);
if (dataGrid != null)
dataGrid.CancelEdit();
}
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null) return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
Thanks to #mm8, I create a behaviour to attach to my buttons to close the editing template.
public static class SwitchCellTemplate
{
/// <summary>
/// Attached property to buttons to close host window
/// </summary>
public static readonly DependencyProperty SwitchTemplate =
DependencyProperty.RegisterAttached
(
"CloseTemplate",
typeof(bool),
typeof(SwitchCellTemplate),
new PropertyMetadata(false, SwithcTemplateChanged)
);
public static bool GetSwitchTemplateProperty(DependencyObject obj)
{
return (bool)obj.GetValue(SwitchTemplate);
}
public static void SetSwitchTemplateProperty(DependencyObject obj, bool value)
{
obj.SetValue(SwitchTemplate, value);
}
public static void SwithcTemplateChanged(DependencyObject property, DependencyPropertyChangedEventArgs args)
{
if (property is Button)
{
Button button = property as Button;
if (button != null) button.Click += OnClick;
}
}
private static void OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button)
{
DataGrid dataGrid = FindParent<DataGrid>((Button)sender);
if (dataGrid != null)
dataGrid.CancelEdit();
}
}
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null) return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
}
So I am trying to make my textbox invisible when a checkbox is not checked. Everything works fine untill I check the box and then uncheck it again. The textbox will stay visible.
private void chbon_Checked_1(object sender, RoutedEventArgs e)
{
if (cchbon.IsChecked == true)
{
txtshow.Visibility = System.Windows.Visibility.Visible;
}
if (chbon.IsChecked == false)
{
txtshow.Visibility = System.Windows.Visibility.Hidden;
}
}
This is the XAML for the Checkbox:
<CheckBox x:Name="chbon" Content="On" HorizontalAlignment="Left" Margin="175,84,0,0" VerticalAlignment="Top" Checked="chbon_Checked_1"/>
<TextBox x:Name="txtshow" HorizontalAlignment="Left" Height="23" Margin="272,82,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="29" Visibility="Hidden"/>
The event Checked does not fire when uncheck happens.
The event Unchecked is for that purpose.
... Checked="chbon_Checked" Unchecked="chbon_Unchecked"/>
and no need to monitor cchbon.IsChecked in code behind:
private void chbon_Checked(object sender, RoutedEventArgs e)
{
txtshow.Visibility = System.Windows.Visibility.Visible;
}
private void chbon_Unchecked(object sender, RoutedEventArgs e)
{
txtshow.Visibility = System.Windows.Visibility.Hidden;
}
Alternatively, you can do it via binding and a converter:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
</Window.Resources>
...
<CheckBox x:Name="chbon"/>
<TextBox x:Name="txtshow" Visibility="{Binding ElementName=chbon, Path=IsChecked,
Converter={StaticResource BoolToVis}, FallbackValue=Hidden}"/>
Note that,
Once you managed this approach you may want to implement a custom converter since the built-in BooleanToVisibilityConverter returns Visible/Collapsed for True/False input (and not Visible/Hidden)
The Checked event handler will only hit when you check the checkbox, not uncheck it. You can also use the Unchecked handler in your XAML that would make the textbox hidden.
private void chbon_Unchecked(object sender, RoutedEventArgs e)
{
txtshow.Visibility = System.Windows.Visibility.Hidden;
}
private void chbon_Checked_1(object sender, RoutedEventArgs e)
{
txtshow.Visibility = System.Windows.Visibility.Visible;
}
when CheckBox goes to unchecked state, Unchecked event fires (simmetric to Checked). Add event handler to both of them.
<CheckBox x:Name="chbon"
Content="On"
HorizontalAlignment="Left"
Margin="175,84,0,0"
VerticalAlignment="Top"
Checked="chbon_Checked_1"
Unhecked="chbon_Checked_1"/>
private void chbon_Checked_1(object sender, RoutedEventArgs e)
{
txtshow.Visibility = cchbon.IsChecked ? Visibility.Visible : Visibility.Hidden;
}
It is common to use binding to boolean property to set Visibility of some element. There is a BooleanToVisibilityConverter in .NET, which returns Visible for true, and Collapsed for false. Collapsed is different from Hidden: Hidden element stil claims the space on the screen as if it was Visible.
There is a way to achieve everything in XAML using a Trigger:
<CheckBox x:Name="chbon" Content="On"
HorizontalAlignment="Left" Margin="175,84,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="txtshow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="23" Width="29" Margin="272,82,0,0" TextWrapping="Wrap">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=chbon}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
<TextBox/>
I have a textblock that contains some non-italicized text. When the mouse enters the textblock, the text changes through the use of the code behind. I would like the code behind to also have the ability to change the text to italicized. This is what I have so far:
XAML:
<TextBlock x:Name="block1"
Background="Cyan"
Foreground="{StaticResource myBrush2}"
Grid.Column="0"
Grid.Row="0"
Height="30"
HorizontalAlignment="Center"
MouseEnter="TextBlock_MouseEnter"
MouseLeave="TextBlock_MouseLeave"
Padding="0,7,0,0"
Text ="Hover Me!"
TextAlignment="Center"
Width="100"/>
Code Behind (C#):
public void TextBlock_MouseEnter(object sender, MouseEventArgs e)
{
string blockName = ((TextBlock)sender).Name;
var block = sender as TextBlock;
if (block != null && blockName == "block1")
{
block.Text = "Yo! I'm TextBlock1";
}
}
I have looked into using System.Drawing and the use of FontStyle.Italic; although I was unsuccessful of actually making it work.
This is what XAML was made for
<TextBlock x:Name="block1"
Background="Cyan"
Foreground="{StaticResource myBrush2}"
Grid.Column="0"
Grid.Row="0"
Height="30"
HorizontalAlignment="Center"
MouseEnter="TextBlock_MouseEnter"
MouseLeave="TextBlock_MouseLeave"
Padding="0,7,0,0"
Text ="Hover Me!"
TextAlignment="Center"
Width="100">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="FontStyle" Value="Italic" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
But, if you really want to, here's an example of how you might implement that functionality from code-behind.
private void block1_MouseEnter(object sender, MouseEventArgs e)
{
SetFontStyle(FontStyles.Italic);
}
private void block1_MouseLeave(object sender, MouseEventArgs e)
{
SetFontStyle(FontStyles.Normal);
}
private void SetFontStyle(FontStyle style)
{
block1.FontStyle = style;
}
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);
};
}
};
I have a button with a context menu, my requirements are to show the context menu on left click
The problem is that the <ContextMenu ItemsSource="{Binding LineItems}" isn't being updated/refresh when the context menu opens. However If i right click first, items are loaded fine
XAML
<Button x:Name="BtnMessageChannel" Click="BtnMessageChannel_Click" Grid.Row="0" Grid.Column="2" Height="23" Width="23" ToolTip="Message Channel" >
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding LineItems}" x:Name="CtxMessageChannel">
<ContextMenu.Resources>
<Image x:Key="img" Source="{Binding Icon}" x:Shared="false"/>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding DisplayName}"/>
<Setter Property="Icon" Value="{StaticResource img}">
</Setter>
</Style>
</ContextMenu.Resources>
</ContextMenu>
</Button.ContextMenu>
<Image Source="Images/mail_send.png" HorizontalAlignment="Left" Width="16" />
</Button>
Code Behind
private void BtnMessageChannel_Click(object sender, RoutedEventArgs e)
{
BtnMessageChannel.ContextMenu.GetBindingExpression(ContextMenu.ItemsSourceProperty)
.UpdateTarget();
BtnMessageChannel.ContextMenu.Visibility = Visibility.Visible;
BtnMessageChannel.ContextMenu.IsOpen = true;
}
Is there any easy solutions to this problem?
An easy solution is to update your button event handler to simulate a right click if the context menu is not currently open.
private void BtnMessageChannel_Click(object sender, RoutedEventArgs e)
{
if (!BtnMessageChannel.ContextMenu.IsOpen)
{
e.Handled = true;
var mouseRightClickEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Right)
{
RoutedEvent = Mouse.MouseUpEvent,
Source = sender,
};
InputManager.Current.ProcessInput(mouseRightClickEvent);
}
}