Display message when ListView contains no items with MVVM in a UWP
Asked Answered
J

3

5

This is my first MVVM project and the code I need to write to manipulate controls in the view somehow seems way too complicated than it has to be.

I'm finding it hard to fully understand MVVM and to decide when I can put stuff in code behind.

Basically my problem is that I want to show a message telling the user that the listview is empty when the ObservableCollection it is bound to contains no items. The idea was to have a TextBlock in the view and only have its visibility property set to Visible when there are no items to display (Before user creates an item and after he deletes all items)

I cannot use this solution as UWP don't support BooleanToVisibilityConverter: WPF MVVM hiding button using BooleanToVisibilityConverter

View:

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:EventMaker3000.View"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ViewModel="using:EventMaker3000.ViewModel"
    xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
    x:Class="EventMaker3000.View.EventPage"
    mc:Ignorable="d">
    <Page.BottomAppBar>
        <CommandBar>
            <CommandBar.Content>
                <Grid/>
            </CommandBar.Content>
            <AppBarButton Icon="Delete" Label="Delete" IsEnabled="{Binding DeletebuttonEnableOrNot}">
                <Interactivity:Interaction.Behaviors>
                    <Core:EventTriggerBehavior EventName="Click">
                        <Core:NavigateToPageAction/>
                        <Core:InvokeCommandAction Command="{Binding DeleteEventCommand}"/>
                    </Core:EventTriggerBehavior>
                </Interactivity:Interaction.Behaviors>
            </AppBarButton>
            <AppBarButton Icon="Add" Label="Add">
                <Interactivity:Interaction.Behaviors>
                    <Core:EventTriggerBehavior EventName="Click">
                        <Core:NavigateToPageAction TargetPage="EventMaker3000.View.CreateEventPage"/>
                    </Core:EventTriggerBehavior>
                </Interactivity:Interaction.Behaviors>
            </AppBarButton>
        </CommandBar>
    </Page.BottomAppBar>

    <Page.DataContext>
        <ViewModel:EventViewModel/>
    </Page.DataContext>

    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!--Header-->
        <TextBlock 
            Text="Events" 
            Foreground="Black" 
            Margin="0,20,0,0" 
            Style="{ThemeResource HeaderTextBlockStyle}" 
            HorizontalAlignment="center" 
            VerticalAlignment="Center"/>

        <ListView
            ItemsSource="{Binding EventCatalogSingleton.Events, Mode=TwoWay}"
            SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"
            Grid.Row="1"    
            Background="WhiteSmoke"
            Padding="0,30,0,0">
            <Interactivity:Interaction.Behaviors>
                <Core:EventTriggerBehavior EventName="SelectionChanged">
                    <Core:InvokeCommandAction Command="{Binding EnableOrNotCommand}"/>
                </Core:EventTriggerBehavior>
            </Interactivity:Interaction.Behaviors>

            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid VerticalAlignment="Center" Margin="5,0">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>

                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>

                        <TextBlock Grid.Column="0"
                            Grid.Row="0"
                            Margin="5" 
                            Text="{Binding Name, Mode=TwoWay}" 
                            Style="{ThemeResource TitleTextBlockStyle}" Foreground="Black"/>
                        <TextBlock Grid.Column="1"
                            Grid.Row="1"
                            Margin="5" 
                            Text="{Binding Place, Mode=TwoWay}"
                            HorizontalAlignment="Right"
                            Style="{ThemeResource CaptionTextBlockStyle}" Foreground="Black"/>
                        <TextBlock Grid.Column="0"
                            Grid.Row="2"
                            Margin="5" 
                            Text="{Binding Description, Mode=TwoWay}"
                            Style="{ThemeResource BodyTextBlockStyle}" Foreground="Black"/>

                        <TextBlock Grid.Column="0"
                            Grid.Row="1" 
                            Margin="5" 
                            Text="{Binding DateTime, Mode=TwoWay}" 
                            Style="{ThemeResource CaptionTextBlockStyle}" Foreground="Black"/>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>

            <!--Sets each listview item to stretch-->
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>

        <!-- TextBlock for empty list view-->

        <TextBlock 
            Grid.Row="1"
            Margin="5,5,5,5"  
            VerticalAlignment="Center"  
            HorizontalAlignment="Center"  
            Text="You have no events"  
            Style="{StaticResource BaseTextBlockStyle}"  
            Visibility="{Binding TextBlockVisibility}"/>
    </Grid>
</Page>

ViewModel:

public class EventViewModel : INotifyPropertyChanged {

private bool _deleteButtonEnableOrNot = false;
private ICommand _enableOrNotCommand;

//TextBlock
private string _textBlockVisibility = "Visible";
private ICommand _textBlockVisibilityCommand;


public EventCatalogSingleton EventCatalogSingleton { get; set; }
public Handler.EventHandler EventHandler { get; set; }

// Disable or enable Deletebutton
public bool DeletebuttonEnableOrNot
{
    get { return _deleteButtonEnableOrNot;}
    set
    {
        _deleteButtonEnableOrNot = value;
        OnPropertyChanged();
    }            
}

public ICommand EnableOrNotCommand
{
    get { return _enableOrNotCommand; }
    set { _enableOrNotCommand = value; }
}

// Set TextBlock visibility
public string TextBlockVisibility
{
    get { return _textBlockVisibility; }
    set
    {
        _textBlockVisibility = value;
        OnPropertyChanged();
    }
}

public ICommand TextBlockVisibilityCommand
{
    get { return _textBlockVisibilityCommand; }
    set { _textBlockVisibilityCommand = value; }
}

// Constructor
public EventViewModel()
{
    //Initializes Date and Time with some values that are bound to controls.
    DateTime dt = System.DateTime.Now;
    _date = new DateTimeOffset(dt.Year, dt.Month, dt.Day, 0, 0, 0, 0, new TimeSpan());
    _time = new TimeSpan(dt.Hour, dt.Minute, dt.Second);

    EventCatalogSingleton = EventCatalogSingleton.getInstance();
    EventHandler = new Handler.EventHandler(this);

    // Creates an instance of the RelayCommand and passes necessary method as a parameter
    _createEventCommand = new RelayCommand(EventHandler.CreateEvent);

    _deleteEventCommand = new RelayCommand(EventHandler.GetDeleteConfirmationAsync);

    _enableOrNotCommand = new RelayCommand(EventHandler.EnableOrNot);

    _textBlockVisibilityCommand = new RelayCommand(EventHandler.TextBlockVisibility);

}

Singleton:

public class EventCatalogSingleton { private static EventCatalogSingleton _instance;

private EventCatalogSingleton()
{
    Events = new ObservableCollection<Event>();

    // Creates instances of events and adds it to the observable collection.
    LoadEventAsync();
}

//Checks if an instance already exists, if not it will create one. Makes sure we only have one instance
public static EventCatalogSingleton getInstance()
{
    if (_instance != null)
    {
        return _instance;
    }
    else
    {
        _instance = new EventCatalogSingleton();
        return _instance;
    }
}

// Creates the observable collection
public ObservableCollection<Event> Events { get; set; }

public void AddEvent(Event newEvent)
{
    Events.Add(newEvent);
    PersistencyService.SaveEventsAsJsonAsync(Events);
}

public void AddEvent(int id, string name, string description, string place, DateTime date)
{
    Events.Add(new Event(id, name, description, place, date));
    PersistencyService.SaveEventsAsJsonAsync(Events);
}


public void RemoveEvent(Event myEvent)
{
    Events.Remove(myEvent);
    PersistencyService.SaveEventsAsJsonAsync(Events);
}

public async void LoadEventAsync()
{

    var events = await PersistencyService.LoadEventsFromJsonAsync();
    if (events != null)
        foreach (var ev in events)
        {
            Events.Add(ev);
        }

}

}

Handler:

public class EventHandler {

public EventViewModel EventViewModel { get; set; }

public EventHandler(EventViewModel eventViewModel)
{
    EventViewModel = eventViewModel;
}

public void CreateEvent()
{
    EventViewModel.EventCatalogSingleton.AddEvent(EventViewModel.Id,    EventViewModel.Name, EventViewModel.Description, EventViewModel.Place, DateTimeConverter.DateTimeOffsetAndTimeSetToDateTime(EventViewModel.Date, EventViewModel.Time));
}


private void DeleteEvent()
{
    EventViewModel.EventCatalogSingleton.Events.Remove(EventViewModel.SelectedEvent);
}

// Confirmation box that prompts user before deletion
public async void GetDeleteConfirmationAsync()
{
    MessageDialog msgbox = new MessageDialog("Are you sure you want to permenantly delete this event?", "Delete event");

    msgbox.Commands.Add(new UICommand
    {
        Label = "Yes",
        Invoked = command => DeleteEvent()
    }
    );

    msgbox.Commands.Add(new UICommand
    {
        Label = "No",
    }
    );
    msgbox.DefaultCommandIndex = 1;
    msgbox.CancelCommandIndex = 1;
    msgbox.Options = MessageDialogOptions.AcceptUserInputAfterDelay;

    await msgbox.ShowAsync();
}

public void EnableOrNot()
{
    EventViewModel.DeletebuttonEnableOrNot = EventViewModel.DeletebuttonEnableOrNot = true;
}

public void TextBlockVisibility()
{
    if (EventViewModel.EventCatalogSingleton.Events.Count < 1)
    {
        EventViewModel.TextBlockVisibility = EventViewModel.TextBlockVisibility = "Visible";
    }        
}

}

Its a lot of code to include, I know - didn't know what to leave out. I included the code for when I enable a delete-button when an item in the listview has been selected - which works fine.

Why doesn't the TextBlock in view show after I delete all items in the listview? And is it really necessary for me to have properties and ICommands in the viewmodel in order to change apperance and other things of controls in the view?

Jase answered 12/2, 2016 at 16:56 Comment(1)
Have you implement the INotifyPropertyChanged correct?Iotacism
A
15

Funny enough, but Daren May and I just taught a free course specifically on this on Microsoft Virtual Academy. It might be a nice resource for you. Look in video #2 @ 13 minutes.

https://mva.microsoft.com/en-US/training-courses/xaml-for-windows-10-items-controls-14483

Check out this simple approach:

With this code:

class VisibleWhenZeroConverter : IValueConverter
{
    public object Convert(object v, Type t, object p, string l) =>
        Equals(0d, (double)v) ? Visibility.Visible : Visibility.Collapsed;

    public object ConvertBack(object v, Type t, object p, string l) => null;
}

And this XAML:

    <StackPanel.Resources>
        <cvt:VisibleWhenZeroConverter x:Name="VisibleWhenZeroConverter" />
    </StackPanel.Resources>

    <ListView ItemsSource="{x:Bind Items}" x:Name="MyList">
        <ListView.Header>
            <TextBlock Visibility="{Binding Items.Count, ElementName=MyList, 
                       Converter={StaticResource VisibleWhenZeroConverter}}">
                <Run Text="There are no items." />
            </TextBlock>
        </ListView.Header>
    </ListView>

Make sense? I hope so.

PS: this answers the EXACT title of your question. Hope it helps.

Best of luck!

Actinopod answered 12/2, 2016 at 22:33 Comment(0)
F
3

First, you want to try your best to keep a clean separation of concerns between your view and your view model. Therefore, try not to include UI specific types like Visibility and MessageDialog. You can create an interface for the MessageDialog that is responsible for showing dialogs and then pass it in to your view model.

Second, you should be prepared to write your own value converters (BooleanToVisibilityConverter), like this one below:

public sealed class BooleanToVisibilityConverter : IValueConverter
{
    public object Convert(object value, 
        Type targetType, object parameter, string language)
    {
        bool isVisible = (bool)value;
        return isVisible ? Visibility.Visible : Visibility.Collapsed;
    }

    public object ConvertBack(object value, 
           Type targetType, object parameter, string language)
    {
        return (Visibility)value == Visibility.Visible;
    }
}

and use it in your view like so:

<Page
   xmlns:converters="using:MyApp.Whatever">
    <Page.Resources>
        <converters:BooleanToVisibilityConverter x:Key="converter"/>
    </Page.Resources>
    <TextBlock
    Visibility="{Binding HasNoItems, Mode=TwoWay, 
        Converter={StaticResource converter}}">
    </TextBlock>
</Page>

and in your VM:

public bool HasNoItems
{
    get { return this.hasNoItems; }
    set { this.hasNoItems = value; OnPropertyChanged(); }
}
Facilitation answered 12/2, 2016 at 19:43 Comment(0)
I
0

I have successfully added visibility bind in a StackPanel for a project of mine like this

Model cs

    Visibility showPanel = Visibility.Collapsed;
    public Visibility ShowPanel
    {
        get
        {
            return showPanel;
        }

        set
        {
            showPanel = value;
            NotifyPropertyChanged("ShowPanel");
        }
    }

XAML

  <StackPanel Height="220" Orientation="Vertical" Visibility="{Binding ShowPanel}">

Also there are many ways you can show a message, for ex

  1. You can put a TextBlock with binding to an error that is "" and when its empty add your error

  2. Create a DialogMessage from within the model

        await Windows.UI.Core.CoreWindow.GetForCurrentThread().Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
          {
              MessageDialog dialog = new MessageDialog(error);
              await dialog.ShowAsync();
          });
    
Iotacism answered 12/2, 2016 at 17:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.