views:

431

answers:

4

Recently, I realized that MVVM pattern is so useful for Silverlight application and studying how to adopt it into my project.

BTW, how to hook up the textbox's textChanged event with Command? There is Command property for Button, however Textbox has no commapd property. If Controls don't have command property, how to combine ICommand and Control's event?

I got following xaml code

<UserControl.Resources>
        <vm:CustomerViewModel x:Key="customerVM"/>    
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" 
          Background="White" 
          DataContext="{Binding Path=Customers, Source={StaticResource customerVM}, Mode=TwoWay}" >

        <StackPanel>
            <StackPanel Orientation="Horizontal"
                        Width="300"
                        HorizontalAlignment="Center">
                <TextBox x:Name="tbName" 
                         Width="50" 
                         Margin="10"/>
                <Button Width="30" 
                        Margin="10" 
                        Content="Find"
                        Command="{Binding Path=GetCustomersByNameCommand, Source={StaticResource customerVM}}"
                        CommandParameter="{Binding Path=Text, ElementName=tbName}"/>
            </StackPanel>
            <sdk:DataGrid ItemsSource="{Binding Path=DataContext, ElementName=LayoutRoot}"
                          AutoGenerateColumns="True"
                          Width="300"
                          Height="300"/>
        </StackPanel>
    </Grid>

What I am trying to do is that if user input some text in the textbox, data will be shown in the datagrid instead of using button click. I know there is autocomplete box control built in. however, I want to know how to call Command property in the ViewModel class in the controls which does not have Command property such as textbox.

Thanks

+3  A: 

Why not just bind the Text property to a property on your view model? That way you get notified when it has changed, and also get the new value:

public string MyData
{
    get { return this.myData; }
    set
    {
        if (this.myData != value)
        {
            this.myData = value;
            this.OnPropertyChanged(() => this.MyData);
        }
    }
}

XAML:

<TextBox Text="{Binding MyData}"/>

HTH,
Kent

Kent Boogaart
please, refer my edited question.
kwon
@kwon, this answer is still valid. You just need to set `UpdateSourceTrigger=PropertyChanged` in your binding, and the VM property will be updated every time the user types a character in the TextBox. So you just have to do whatever you need to do in the property setter
Thomas Levesque
@Thomas, UpdateSourcetrigger is WPF, doesn't work in Silverlight.
Jeremy Likness
Uh, didn't know that... I can't believe the number of features (including major ones) that are missing from Silverlight
Thomas Levesque
+1  A: 

For the sake of conversation, lets say that you do need to hook up some arbitrary event to a Command rather than bind directly to a property on the ViewModel (due to a lack of support in the control or framework, defect, etc.) This can be done in the codebehind. Contrary to some misconceptions, MVVM does not preclude codebehind. It is just important to remember that the logic in the codebehind should not cross layers - it should be relate directly to the UI and the specific UI technology being used. (Note however, that putting 95% of your work in the markup file may make it a little unintutitive to have some functionality in the codebehind, so a comment or two in the markup about this one-off codebehind implementation may make things easier down the road for yourself or others.)

There are usually 2 parts to binding a command in codebehind. First, you have to respond to the event. Second, you (may) want to tie into the command's CanExecute property.

    // Execute the command from the codebehind
    private void HandleTheEvent(Object sender, EventArgs e)
    {
        var viewModel = DataContext as ViewModel;
        if (viewModel != null)
        {
            var command = viewModel.SomeCommand;
            command.Execute(null);
        }
    }

    // Listen for the command's CanExecuteChanged event - remember to call this (and unhook events as well) whenever the viewmodel instance changes
    private void ListenToCommandEvent()
    {
        var viewModel = DataContext as ViewModel;
        if (viewModel != null)
        {
            var command = viewModel.SomeCommand;
            command.CanExecuteChanged += (o, e) => EnableOrDisableControl(command.CanExecute(null)); ;
        }
    }
avidgator
A: 

You should use a Behavior to execute the Command:

public class CommandBehavior : TriggerAction<FrameworkElement>
{
    public static readonly DependencyProperty CommandBindingProperty = DependencyProperty.Register(
        "CommandBinding",
        typeof(string),
        typeof(CommandBehavior),
        null);

    public string CommandBinding
    {
        get { return (string)GetValue(CommandBindingProperty); }
        set { SetValue(CommandBindingProperty, value); }
    }

    private ICommand _action;

    protected override void OnAttached()
    {
        DataContextChangedHandler.Bind(AssociatedObject, _ProcessCommand);
    }

    private void _ProcessCommand(FrameworkElement obj)
    {
        if (AssociatedObject != null)
        {

            var dataContext = AssociatedObject.DataContext;

            if (dataContext != null)
            {
                var property = dataContext.GetType().GetProperty(CommandBinding);
                if (property != null)
                {
                    var value = property.GetValue(dataContext, null);
                    if (value != null && value is ICommand)
                    {
                        _action = value as ICommand;
                        if (AssociatedObject is Control)
                        {
                            var associatedControl = AssociatedObject as Control;
                            associatedControl.IsEnabled = _action.CanExecute(null);
                            _action.CanExecuteChanged +=
                                (o, e) => associatedControl.IsEnabled = _action.CanExecute(null);
                        }

                    }
                }
            }
        }
    }

    protected override void Invoke(object parameter)
    {
        if (_action != null && _action.CanExecute(parameter))
        {
            _action.Execute(parameter);
        }
    }
}

public static class DataContextChangedHandler
{
    private const string INTERNAL_CONTEXT = "InternalDataContext";
    private const string CONTEXT_CHANGED = "DataContextChanged";

    public static readonly DependencyProperty InternalDataContextProperty =
        DependencyProperty.Register(INTERNAL_CONTEXT,
                                    typeof(Object),
                                    typeof(FrameworkElement),
                                    new PropertyMetadata(_DataContextChanged));

    public static readonly DependencyProperty DataContextChangedProperty =
        DependencyProperty.Register(CONTEXT_CHANGED,
                                    typeof(Action<FrameworkElement>),
                                    typeof(FrameworkElement),
                                    null);


    private static void _DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var control = (FrameworkElement)sender;
        var handler = (Action<FrameworkElement>)control.GetValue(DataContextChangedProperty);
        if (handler != null)
        {
            handler(control);
        }
    }

    public static void Bind(FrameworkElement control, Action<FrameworkElement> dataContextChanged)
    {
        control.SetBinding(InternalDataContextProperty, new Binding());
        control.SetValue(DataContextChangedProperty, dataContextChanged);
    }
}

Now you can "Bind" your command in xaml:

        <TextBox Text="{Binding SearchText, Mode=TwoWay}" >
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="TextChanged">
                    <utils:CommandBehavior CommandBinding="SearchCommand" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBox>

If you need you can extend this Behavior with extra properties, for example if you need the sender or the DataContext of a different element..

Kind regards, Tamás

(I have found this on a blog post, but I can not remember for it's address)

abtamas
Thanks your replyIt's so complicated code to do something very simple. I am disappointing silverlight. Anyway, is there any framework to reduce complex code and to solve this kind of problems for MVVM?
kwon
+1  A: 

Here's the easiest way. Bind your text box to the property on the view model, as you described above. Then, simply add a code-behind (yes, I'm talking code-behind with MVVM, it's not the end of the world) event to the Text Box. Add a TextChanged event, then simply refresh the binding.

Altogether, you'll have something like this for a view model:

public class MyViewModel 
{
    private string _myText;

    public string MyText 
    {
        get { return _myText; }
        set 
        {
            _myText = value;
            RaisePropertyChanged("MyText"); // this needs to be implemented
            // now do whatever grid refresh/etc
        }
    }
}

In your XAML, you'll have this:

<TextBox Text="{Binding MyText,Mode=TwoWay}" TextChanged="TextBox_TextChanged"/>

Finally, in the code behind, simply do this:

public void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
   var binding = ((TextBox)sender).).GetBindingExpression(TextBox.TextProperty);
   binding.UpdateSource();
}

That will cause your property to update anytime the text changes. }

Jeremy Likness
Thanks Jeremy, it's the way to solve my problem, however, I am trying to reduce codes in the code-behind.Isn't there any way to do so without code-behind?
kwon
The question is ... why? Why are you trying to reduce code-behind. What's the difference between having an event in the code behind, or coming up with an attached behavior that you specify in XAML? You still have code-behidn, in one case it is explicit (the behavior), the other implicit (the code-behind file). You can solve this with a Trigger but I'm just not sure why you'd want to do the extra work.
Jeremy Likness
I have to agree with Jeremy MVVM is not about reducing Code-behind or eliminating it, its really about a separation of concerns and testability. Putting UI code in the code-behind makes perfect sense, since it is UI code and typically not something you will be testing.
Agies