简体   繁体   中英

Hide ListView when no results in ItemsSource

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM