Search code examples
c#wpfdata-bindingmvvm-light

Preventing null value insertion through a RelayCommand in WPF MVVM?


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.


Solution

  • 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.