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?
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!
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(); }
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With