Good day, I am new to C# and WPF. Currently, I am trying to create a Todo list, but I am having problems with the CanExecuteAddTodoCommand() for my RelayCommand. I used MVVMlight for this and Material design in XAML for the XAML.
This is the code I'm working with.
Model:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.ObjectModel;
using System.Windows;
using ToDoV3.Model;
namespace ToDoV3.ViewModel
{
/// <summary>
/// This class contains properties that the main View can data bind to.
/// <para>
/// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
/// </para>
/// <para>
/// You can also use Blend to data bind with the tool's support.
/// </para>
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class MainViewModel : ViewModelBase
{
private ObservableCollection<TodoModel> _todoList;
public ObservableCollection<TodoModel> TodoList
{
get { return _todoList; }
set { Set(ref _todoList, value); }
}
public MainViewModel()
{
TodoList = new ObservableCollection<TodoModel>();
}
private string _newTodo;
public string NewTodo
{
get { return _newTodo; }
set { Set(ref _newTodo, value); }
//Button Press Trigger
private RelayCommand _addTodoCommand;
public RelayCommand AddTodoCommand
{
get
{
return _addTodoCommand
?? (_addTodoCommand = new RelayCommand(ExecuteAddTodoCommand, CanExecuteAddTodoCommand));
}
}
//Adds the todo on clicking the button or pressing enter
public void ExecuteAddTodoCommand()
{
TodoList.Add(new TodoModel { TaskTodo = NewTodo, IsDone = false });
//Clears the box after pressing plus or enter
if (NewTodo != string.Empty)
{
NewTodo = string.Empty;
}
}
public bool CanExecuteAddTodoCommand()
{
if (NewTodo == string.Empty)
{
MessageBox.Show("please enter a note!");
return false;
}
else
{
return true;
}
}
}
}
View
<Window x:Class="ToDoV3.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ToDoV3"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
DataContext="{Binding Main, Source={StaticResource Locator}}"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
Background="{DynamicResource MaterialDesignPaper}"
TextElement.FontWeight="Medium"
TextElement.FontSize="14"
FontFamily="{materialDesign:MaterialDesignFont}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
<RowDefinition Height="85"/>
</Grid.RowDefinitions>
<!--Where tasks are displayed-->
<ScrollViewer>
<ItemsControl
Margin="12,0,12,0"
Grid.IsSharedSizeScope="True"
ItemsSource="{Binding TodoList}">
<ItemsControl.ItemTemplate>
<DataTemplate
>
<Border
x:Name="Border"
Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
SharedSizeGroup="Checkerz" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox
VerticalAlignment="Center"
IsChecked="{Binding IsDone}" />
<StackPanel
Grid.Column="1"
Margin="8,0,0,0">
<TextBlock
FontWeight="Bold"
Text="{Binding TaskTodo}" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding IsSelected}"
Value="True">
<Setter
TargetName="Border"
Property="Background"
Value="{DynamicResource MaterialDesignSelection}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!--This holds the status count-->
<Grid Grid.Row="1" Background="#212121" >
<TextBlock Foreground="#FFF"
Margin="10 7 0 0"
FontSize="12"
Text="{Binding CountStatus}"/>
</Grid>
<!--This holds the textbox and button-->
<Grid Grid.Row="2" Background="#212121">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
Margin="10 0 10 0"
Background="#FFF"
Height="70"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="Type your todo here"
IsEnabled="{Binding Path=IsChecked, ElementName=MaterialDesignOutlinedTextBoxEnabledComboBox}"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" >
<TextBox.Text>
<Binding Path="NewTodo" UpdateSourceTrigger="PropertyChanged"/>
</TextBox.Text>
<TextBox.InputBindings>
<KeyBinding Command="{Binding AddTodoCommand}" Key="Enter"/>
</TextBox.InputBindings>
</TextBox>
<Button
Command="{Binding AddTodoCommand}"
Grid.Column="1"
IsEnabled="{Binding DataContext.ControlsEnabled, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
Style="{StaticResource MaterialDesignFloatingActionLightButton}"
ToolTip="MaterialDesignFloatingActionLightButton">
<materialDesign:PackIcon Kind="Plus" Width="24" Height="24"/>
</Button>
</Grid>
</Grid>
</Window>
This is where I specifically have a problem with. This is the first method I tried.
public bool CanExecuteAddTodoCommand()
{
if (NewTodo == string.Empty)
{
MessageBox.Show("please enter a note!");
return false;
}
else
{
return true;
}
}
The output I expect is that whenever the add button is pressed it would only work if NewTodo is not empty. It works but it would pass through a blank and would appear in the note list after that, it would not pass through input if the textbox were blank.
I've tried switching it to null since I think the reason it passes it's because it's null rather than empty. So, I changed it to something like this.
public bool CanExecuteAddTodoCommand()
{
if (string.IsNullOrEmpty(NewTodo))
{
MessageBox.Show("please enter a note!");
return false;
}
else
{
//return NewTodo != string.Empty || NewTodo == null;
return true;
}
}
It now sees that the textbox is null and sent the message box, but I only want this prompt to appear only when I triggered a push. The message box would immediately show, and the button cannot be pressed. Also, for some reason I can pass through what's inside the textbox to the observableobject and it would be displayed by pressing enter but as mentioned earlier, the code above does disable the button so I can't send anything through it. Lastly, there are parts of the code mentioning a checkbox but that's not related to this question.
Any thoughts on this would be highly appreciated.
TLDR: Tried two ways of implementing the CanExecuteAddTodoCommand().
This one is almost correct albeit with one problem. It would pass a null since the if else statement only checked for empty not null.
The second CanExecuteAddTodoCommand() was changed to see if the NewTodo was null, the message would send by pressing enter but it would not allow me to send using the textbox.
If you want to disable the command when there is no text in the TextBox
, you should implement the CanExecuteAddTodoCommand
method something like this:
public bool CanExecuteAddTodoCommand() => !string.IsNullOrEmpty(NewTodo);
But if you want a "prompt to appear only when you triggered a push", you should always enable the command and display the MessageBox
in the Execute
method:
public void ExecuteAddTodoCommand()
{
if (string.IsNullOrEmpty(NewTodo))
{
MessageBox.Show("please enter a note!");
return false;
}
TodoList.Add(new TodoModel { TaskTodo = NewTodo, IsDone = false });
//Clears the box after pressing plus or enter
if (NewTodo != string.Empty)
{
NewTodo = string.Empty;
}
}
public bool CanExecuteAddTodoCommand() => true;
You cannot control when the framework calls the CanExecute
method so displaying a MessageBox
in this method is a bad idea.
Also remember to refresh the status command in the setter of the NewTodo
property:
private string _newTodo;
public string NewTodo
{
get { return _newTodo; }
set
{
Set(ref _newTodo, value);
_addTodoCommand.RaiseCanExecuteChanged();
}
}
This raises the CanExecuteChanged
event which will cause the framework to call the CanExecute
method to determine whether the command should still be enabled or disabled.