2

I'm new in C# and created a simple app that works fine, but I wanna to learn C# using the pattern MVVM. So I'm trying to migrate my app to MVVM and I become confused

My Goal is simple:

1) When opened, the app scan a folder and index all the files in the format "[number] [name]" - It's working fine!

2) I have a window with just one textBox. The user types a number and press ENTER. In this moment I have a CatalogViewModel that is a set o File and should select the file specified by the number in textBox and open it.

Problem 1: In MVVM I can't pass the data from my view Main to my ViewModel CatalogViewModel (I'm not sure if I made correct)

Problem 2: I can't handle the ENTER key and trigger a function inside CatalogViewModel

I'm a little confused with MVVM and can't go ahead. I know that it's simple. Please, how to resolve this 2 problem (in detail please, I'm a beginner in C# and in all its concept)

UPDATE 1:

tryed the janonimus' Solution to problem 1 But the databind is just ONE WAY. The value from VM goes to the VIEW, but the changes on the VIEW don't to to VM. I've implemented INotifyPropertyChanged this way

using Prism.Mvvm;
...    

    public class CatalogViewModel: BindableBase
    {

        private string selectedValue = "100";
        public string SelectedValue
        {
            get { return selectedValue; }
            set { SetProperty(ref selectedValue, value); }
        }

But the Databind becomes just ON WAY the XAML

<TextBox x:Name="tbSelectedValue" Text="{Binding SelectedValue, Mode=TwoWay}"

UPDATE 2

I found the solution for the problem 1. The code provided by janonimus works just in ONE WAY because the default behavior of TextBox.Text is to upadte when it lose the focus, but in my case it will never lose the focus see this post

The following code resolved the problem 1:

Text="{Binding Path=SelectedValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}

The problem 2 Can be resolved with Pedro Silva's answer

if (e.Key == Key.Enter && tbSelectedValue.Text != String.Empty)
            {
                vm.OpenSelectedFile();
                tbSelectedValue.Text = String.Empty;
            }

But I wanna to implement it using a more sofisticated way, using ICommand. Following the advise sent by janonimus, I created the BaseCommand Class Exactly like this but it throws a mismatch error when I call the function OpenSelectedFile

private BaseCommand<CatalogViewModel> _selectFileCommand;
public ICommand SelectFileCommand
{
    get
    {
        if (_selectFileCommand == null)
        {
            _selectFileCommand = new BaseCommand<CatalogViewModel>(OpenSelectedFile, true);
        }
        return _selectFileCommand;
    }
}
public void OpenSelectedFile()
{
    try
    {
        OpenFileByNumber(Int32.Parse(SelectedValue));
    }
    catch (Exception e)
    {
        MessageBox.Show("Número inválido: \'" + SelectedValue + "\"",
           "ERRO", MessageBoxButton.OK, MessageBoxImage.Warning);
    }

}

That's my code for reference...

Main.xaml.cs

namespace SLMT.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class Main : Window
    {

        public Main()
        {
            InitializeComponent();
            DataContext = new CatalogViewModel();
            chosenNumber.Focus();
        }



        // Permite inserir somente números
        private void ChosenNumber_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            Regex regex = new Regex("[^ 0-9]+");
            e.Handled = regex.IsMatch(e.Text);
        }

        private void ChosenNumber_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter && chosenNumber.Text != String.Empty)
            {
                //catalog.OpenFileByNumber(ConvertToInt(numeroEscolhido.Text));
                //catalog.OpenSelectedFile(); // Will become someting like this
                chosenNumber.Text = String.Empty;            

            }
        }

        private int ConvertToInt(string value)
        {
            try
            {
                var str = value.Replace(" ", String.Empty);
                return Int32.Parse(str);
            }
            catch (Exception exc)
            {
                MessageBox.Show("O número: \"" + chosenNumber.Text + "\" é inválido", "ERRO", MessageBoxButton.OK, MessageBoxImage.Error);
                chosenNumber.Text = String.Empty;

                return 0;
            }
        }


        /// <summary>
        /// Controll what wil lhappen if some KEYS are pressed on APP
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void GMain_KeyUp(object sender, KeyEventArgs e)
        {

            switch (e.Key){

                case Key.Escape:
                    Environment.Exit(0);
                    break;

                case Key.F1:
                    //wListFiles = new ListFiles(catalog);
                    //wListFiles.ShowDialog();
                    //numeroEscolhido.Text = wListFiles.SelectFile();
                    //numeroEscolhido.SelectAll();
                    break;
            }


        }
    }
}

ps: The commented lines, I imported from the version without MVVM

Main.xaml

<Window x:Name="wMain" x:Class="SLMT.Views.Main"
        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:SLMT.Views"
        mc:Ignorable="d"
        Title="Ministério Tons" Height="364" Width="700" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" WindowStyle="None">
    <Grid x:Name="gMain" KeyUp="GMain_KeyUp">
        <Image x:Name="imgBackground" HorizontalAlignment="Left" Height="364" VerticalAlignment="Top" Width="700" Source="/SLMT;component/Resources/img/background2.jpg" Opacity="100"/>
        <TextBox x:Name="chosenNumber" HorizontalAlignment="Center" Height="34" Margin="500,294,56,36" TextWrapping="Wrap" VerticalAlignment="Center" Width="144" BorderBrush="{x:Null}" Background="{x:Null}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" UndoLimit="50" ForceCursor="True" PreviewTextInput="ChosenNumber_PreviewTextInput" KeyUp="ChosenNumber_KeyUp" BorderThickness="0" FontSize="20" Opacity="0.6" FontWeight="Bold"/>

    </Grid>
</Window>

And the Relevant part of CatalogViewModel.cs

namespace SLMT.ViewModel
{
    public class CatalogViewModel: ObservableCollection <File>
    {

        private int selectedNumber;

        /// <summary>
        /// Contain the selected number in the View
        /// </summary>
        public int SelectedNumber
        {
            get { return selectedNumber; }
            set { selectedNumber = value; }
        }


        // REMOVED CODE TO SCAN AND INDEX THE FILES



        public CatalogViewModel() : base()
        {
            ScanFiles();
            ValidateAndAddFiles();
            ShowAlerts();
        }





        public void OpenSelectedFile()
        {
            OpenFileByNumber(SelectedNumber);
        }


        /// <summary>
        /// Get the file from catalog identified my the number
        /// </summary>
        /// <param name="number"></param>
        /// <returns>File|null</returns>
        private File GetFileByNumber(int number)
        {
            foreach (var file in this)
            {
                if (file.number == number){
                    return file;
                }
            }

            return null;
        }

        private void OpenFileByNumber(int number)
        {
            var file = GetFileByNumber(number);

            if (file == null)
            {
                MessageBox.Show("Nenhum arquivo encontrado com o número: \'" + number +"\"",
                   "ARQUIVO NÃO ENCONTRADO", MessageBoxButton.OK, MessageBoxImage.Warning);
            } else
            {
                file.Open();
            }
        }                
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Henry Ávila
  • 426
  • 4
  • 12
  • Possible duplicate of [Binding a WPF ShortCut Key to a Command in the ViewModel](http://stackoverflow.com/questions/2382916/binding-a-wpf-shortcut-key-to-a-command-in-the-viewmodel) – Lynn Crumbling Mar 30 '17 at 20:35

2 Answers2

1

Try the following to have access to the view model that you created in your constructor.

CatalogViewModel vm = new CatalogViewModel();

public Main()
{
    InitializeComponent();
    DataContext = vm;
    chosenNumber.Focus();
}

Then in your key handler, you can do this:

private void ChosenNumber_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter && chosenNumber.Text != String.Empty)
    {
        //catalog.OpenFileByNumber(ConvertToInt(numeroEscolhido.Text));
        //catalog.OpenSelectedFile(); // Will become someting like this

        vm.OpenSelectedFile();
        chosenNumber.Text = String.Empty;            
    }
}

Update: Using ICommand

Using the BaseCommand class you pointed to from here. I added the following code to the CatalogViewModel and got it working. You had the type in BaseCommand as the view model, but that's supposed to be the type of the command parameter (based on examples in that post).

private BaseCommand<object> _selectFileCommand;
public ICommand SelectFileCommand
{
    get
    {
        if (_selectFileCommand == null)
        {
            _selectFileCommand = new BaseCommand<object>((commandParam) => OpenSelectedFile(commandParam),
                                                         (commandParam) => CanOpenSelectedFile(commandParam));
        }
        return _selectFileCommand;
    }
}

public void OpenSelectedFile(object commandParam = null)
{
    Debug.WriteLine("CatalogViewModel.OpenSelectedFile was called.");
    Debug.WriteLine("SelectedValue = " + this.SelectedValue);
}

public bool CanOpenSelectedFile(object commandParam = null)
{
    return true;
}

Once you get into the OpenSelectedFile method, you should be able to hook it up to the functionality that you want.

Community
  • 1
  • 1
Pedro Silva
  • 687
  • 6
  • 14
  • It Works, Thanks, but the dataBind from view to ViewModel is not working (see UPDATE1) – Henry Ávila Mar 31 '17 at 15:13
  • Keep in mind that this would violate MVVM, as the view is now directly tied to the VM. – Bradley Uffner Mar 31 '17 at 18:28
  • Yes Bradley, and I don't wanna this. I wanna to do in "FULL MVVM". In update 2 I wrote a try (withou success) to resolve this problem (problem 2). Can you help me? – Henry Ávila Mar 31 '17 at 18:32
  • The reason you're not seeing the SelectedValue change is that by default the binding to Textbox.Text only triggers an update when the textbox loses focus. By handling the Enter key press, it never loses focus, so the value isn't updated. You can change your binding to: Text="{Binding SelectedValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}". That should work as you'd expect. – Pedro Silva Mar 31 '17 at 18:34
  • Yes Pedro Silva, the **problem 1 is resolved**. But The **problem 2 is not resolved**, Can you help me to resolve using "FULL MVVM" implementation? **(see UPDATE 2)** – Henry Ávila Mar 31 '17 at 18:37
  • @BradleyUffner correct, that's not full mvvm but it's helping him debug through his issues. He had a couple. Once this is working correctly, then he can follow janonimus's recommendation to use keybindings to replace the KeyUp handler. – Pedro Silva Mar 31 '17 at 18:38
  • @HenryÁvila Sure, what error/exception are you getting? are you able to get into your OpenSelectedFile method at all? – Pedro Silva Mar 31 '17 at 20:41
1

You need to do a few things here.

  1. For problem 1, you must bind the View that will pass the data to ViewModel. In this case, the TextBox.Text property must bind to CatalogViewModel.SelectedNumber property:

    <TextBox x:Name="chosenNumber" 
         ...
         Text={Binding SelectedNumber} />
    
  2. For MVVM's sake, the CatalogViewModel class must implement the INotifyPropertyChanged interface. And just create a property for the ObservableCollection that you have.

  3. For problem 2, you're going to need KeyBinding and ICommand to make this work. In the View, it should look like this:

    <TextBox x:Name="chosenNumber" 
     ...
     Text={Binding SelectedNumber}>
        <TextBox.InputBindings>    
            <KeyBinding Key="Enter"
                        Command="{Binding SelectFileCommand}" />
        </TextBox.InputBindings>
    </TextBox>
    

In the ViewModel, you will need an ICommand property:

public class CatalogViewModel: INotifyPropertyChanged
{
        private BaseCommand _selectFileCommand;
        public ICommand SelectFileCommand
        {
            get
            {
                if (_selectFileCommand == null)
                {
                    _selectFileCommand = new BaseCommand(SelectFile, CanSelectFile);
                }
                return _selectFileCommand;
            }
        }
...

Where, SelectFile is a function that will do the action, CanSelectFile is function tells whether or not the command can execute, BaseCommand is an implementation of the ICommand interface. You can refer to this question: WPF selectedItem on Menu or get commandparameter in viewmodel

UPDATE: Use this BaseCommand implementation, and not the BaseCommand<T>:

class BaseCommand : ICommand
{
    private readonly Action _executeMethod = null;
    private readonly Func<bool> _canExecuteMethod = null;

    public BaseCommand(Action executeMethod, Func<bool> canExecuteMethod)
    {
        _executeMethod = executeMethod;
        _canExecuteMethod = canExecuteMethod;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute()
    {
        if (_canExecuteMethod != null)
        {
            return _canExecuteMethod();
        }
        return true;
    }

    public void Execute()
    {
        if (_executeMethod != null)
        {
            _executeMethod();
        }
    }
}

Give this a shot and let us know what happens.

Community
  • 1
  • 1
janonimus
  • 1,911
  • 2
  • 11
  • 11
  • janonimus, I've adapted your solution to Probelm 1 and its working, thanks. About the **problem 2**, I've made as you sugested me but I'm facing a problem. Please, **see UPDATE 2** for mor details – Henry Ávila Mar 31 '17 at 18:33
  • You are getting the mismatch error because you used the `BaseCommand` implementation. I update my answer for the appropriate `BaseCommand` implementation. – janonimus Apr 01 '17 at 10:49