2

So I have a View with two subviews. One of the subviews is an on screen keyboard with textbox. Below that are some buttons which are part of a different subview. See below:

keyboard and buttons

When I press the keyboard buttons it types in the textbox. Both the subview with the buttons and the subview with the keyboard have their own ViewModels. My question is, how do I reference the keyboard view from the button view (so I can get the contents of the text field, for example, or clear it if the user clicks "Go Back").

I'm trying to conceptualize it, but I can't figure out how I would get the same instance of the ViewModel of the keyboard that the Main View has.

I can create a variable:

private KeyboardViewModel keyboard;

But how do I instantiate that variable with the instance that the Main View already has (so I can access those properties from the button viewmodel)?

aufty
  • 407
  • 2
  • 9
  • 26
  • 4
    Dependency injection? – Sievajet Sep 15 '15 at 21:01
  • 1
    Can you show how you initialize your view? – Lorek Sep 15 '15 at 21:06
  • I have a UserControl on the Main View called "Container". I set the Container to different views that I have saved as UserControls. Within these views there are subviews, like the keyboard and the buttons. For example: private List screens = new List() {new IntroScreen(), new NavScreen(), new QwertyKeyboard()}; – aufty Sep 15 '15 at 21:13
  • @aufty - That sounds pretty unnecessary. You should just use a ContentPresenter control, then use DataTemplates to specify which user control should be created depending on the datatype. – user3690202 Sep 15 '15 at 21:16
  • This is just a design problem as you misplaced your data. You have 3 seperate ViewModel and you have ONE data for display and process. All you need to do is to extract out that one piece of data out from the ViewModel and create a singleton DataCache-like instance. Then all of your view models will have access to the data source. – cscmh99 Sep 16 '15 at 01:23

3 Answers3

5

The main problem is that you misplaced your datasource in one of your ViewModel when the datasource is actually needed to be reuse in multiple View/ViewModel. What you need to do is to refactor the datasource out into a singleton instance or an seperate instance that can be injected into different ViewModels' constructor. By decoupling out the datasource from a particular ViewModel can give it freedom for different place to access.

public class DataCache
{
    private static DataCache singletonInstance;

    // You can have freedom to choose the event-driven model here
    // Using traditional Event, EventAggregator, ReactiveX, etc
    public EventHandler OnMessageChanged;

    private DataCache()
    {

    }

    public static DataCache Instance
    {
        get { return singletonInstance ?? (singletonInstance = new DataCache()); }
    }

    public string OnScreenMessage { get; set; }

    public void AddStringToMessage(string c)
    {
        if (string.IsNullOrWhiteSpace(c)) return;

        OnScreenMessage += c;
        RaiseOnMessageChanged();
    }

    public void ClearMessage()
    {
        OnScreenMessage = string.Empty;
        RaiseOnMessageChanged();
    }

    private void RaiseOnMessageChanged()
    {
        if (OnMessageChanged != null)
            OnMessageChanged(null, null);            
    }
}

public class MainViewModel : ViewModelBase
{
    private readonly MessageViewModel messageVM;
    private readonly KeyboardViewModel keyboardVM;
    private readonly ButtonsViewModel buttonsVM;

    private readonly DataCache dataCache;

    public MainViewModel()
    {
        messageVM = new MessageViewModel();
        keyboardVM = new KeyboardViewModel();
        buttonsVM = new ButtonsViewModel();
    }

    public ViewModelBase MessageViewModel { get { return messageVM; } }
    public ViewModelBase KeyboardViewModel { get { return keyboardVM;  } }
    public ViewModelBase ButtonsViewModel { get { return buttonsVM; } }
}

public class MessageViewModel : ViewModelBase
{
    private readonly DataCache dataCache = DataCache.Instance;

    public MessageViewModel()
    {
        dataCache.OnMessageChanged += RaiseMessageChanged;
    }

    private void RaiseMessageChanged(object sender, EventArgs e)
    {
        OnPropertyChanged("Message");
    }

    public string Message
    {
        get { return dataCache.OnScreenMessage; }
        set { dataCache.OnScreenMessage = value; }
    }
}

public class KeyboardViewModel : ViewModelBase
{
    private readonly DataCache dataCache = DataCache.Instance;

    private ICommand onClickButtonCommand;
    public ICommand OnClickButton
    {
        get
        {
            return onClickButtonCommand ?? (onClickButtonCommand = new RelayCommand(p => dataCache.AddStringToMessage((string)p))); 
        }
    }
}

public class ButtonsViewModel : ViewModelBase
{
    private readonly DataCache dataCache = DataCache.Instance;

    private ICommand onGoBackCommand;
    public ICommand OnGoBackButton
    {
        get
        {
            return onGoBackCommand ?? (onGoBackCommand = new RelayCommand(p => dataCache.ClearMessage()));
        }
    }
}

public class RelayCommand : ICommand
{
    #region Fields

    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    #endregion Fields

    #region Constructors

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged;

    public void RaiseCanExecuteChanged()
    {
        var handler = CanExecuteChanged;
        if (handler != null) handler(this, EventArgs.Empty);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }


    #endregion ICommand Members
}

<Window x:Class="StudentScoreWpfProj.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:StudentScoreWpfProj"        
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d"
    d:DataContext="{d:DesignInstance Type=local:MainViewModel,IsDesignTimeCreatable=True}"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <local:MessgaeView DataContext="{Binding MessageViewModel}" />
    <local:KeyboardView Grid.Row="1" DataContext="{Binding KeyboardViewModel}" />
    <local:ButtonsView Grid.Row="2" DataContext="{Binding ButtonsViewModel}" />
</Grid>

<UserControl x:Class="StudentScoreWpfProj.ButtonsView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:StudentScoreWpfProj"
         mc:Ignorable="d" 
         d:DataContext="{d:DesignInstance Type=local:ButtonsViewModel,IsDesignTimeCreatable=True}"
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
    <StackPanel Orientation="Horizontal">
        <Button Content="GoBack" Command="{Binding OnGoBackButton}"></Button>
        <Button Content="Continue"></Button>
    </StackPanel>
</Grid>

<UserControl x:Class="StudentScoreWpfProj.KeyboardView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:StudentScoreWpfProj"
         mc:Ignorable="d" 
         d:DataContext="{d:DesignInstance Type=local:KeyboardViewModel,IsDesignTimeCreatable=True}"
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
     <StackPanel Orientation="Horizontal">
        <Button Content="A" Command="{Binding OnClickButton}" CommandParameter="A"></Button>
        <Button Content="B" Command="{Binding OnClickButton}" CommandParameter="B"></Button>
        <Button Content="C" Command="{Binding OnClickButton}" CommandParameter="C"></Button>
    </StackPanel>
</Grid>

<UserControl x:Class="StudentScoreWpfProj.MessgaeView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:StudentScoreWpfProj"
         mc:Ignorable="d" 
         d:DataContext="{d:DesignInstance Type=local:MessageViewModel,IsDesignTimeCreatable=True}"
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
    <TextBox Text="{Binding Message}"/>
</Grid>

cscmh99
  • 2,701
  • 2
  • 15
  • 18
  • When you say "datasource" do you mean the properties from the ViewModel? – aufty Sep 17 '15 at 14:57
  • Your data is the text in textbox. The data is shared by different viewmodel. That's why the logical design is to store the data outside viewmodel. – cscmh99 Sep 18 '15 at 01:03
  • Right now I've added the child VMs as properties. They still contain their own properties but the parent has access to them now. Is this an OK way to do it? Looks like that's what you did above... except I'm using dot notation to get to the properties of the children. – aufty Sep 18 '15 at 19:02
  • @aufty what if you have yet ANOTHER view model need to access the text in textbox ? It Is a design problem. Your data is too tightly couple with one of your view model. – cscmh99 Sep 19 '15 at 06:24
2

You could do several things ...

You could create a static instance for easy access, and expose what you want on it (not recommended, read comments).

You could use dependency injection, so your other viewmodel will take the keyboard viewmodel as a parameter (please have a look at my other answer, it'll get you started quicly).

You could use a messenger to help you talk between them as well. most mvvm frameworks will have some ( have a look at this SO question, and at this code project article to get you started. They are specifically for MVVM light, but they'll help you understand the concept) .

Community
  • 1
  • 1
Noctis
  • 11,507
  • 3
  • 43
  • 82
  • The only part of this answer that I agree with (and the reason why I didn't downvote it) was that you mentioned dependency injection. Static instances are horrible, and the messenger part is pretty vague to say the least. – user3690202 Sep 15 '15 at 21:14
  • @user3690202 static instances ARE horrible ... that doesn't mean they are not useful, or not an option. The messenger part ... hmmm ... well ... it depends if he's using one to begin with . If not, no point talking about it, but it's an mvvm option to talk across VM's – Noctis Sep 15 '15 at 21:15
  • rewriting the entire application in assembler is technically "an option". I tend to restrict the options I suggest to those which are sensible and good practice. Trying to make the world a better place - who knows, maybe I'll need to maintain some of this code one day :) – user3690202 Sep 15 '15 at 21:18
  • I've read about messengers but I haven't started using one yet. I'm slowly trying to get a handle on what I'm doing with WPF / MVVM. I just realized I don't know how to "get" the instance of the viewmodel that the view is bound to. I'm trying to read about Dependency Injection right now. – aufty Sep 15 '15 at 21:19
1

How about using ServiceLocator from Microsoft.Practices.ServiceLocation?

ServiceLocator.Current.GetInstance<ViewModelName>(); 
DiSaSteR
  • 608
  • 6
  • 11