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 (ie 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);
};
}
};
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.