Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM: How to make a function call on a control?

Tags:

c#

mvvm

wpf

In XAML, I have a TextBox with x:Name of MyTextBox.

<TextBox x:Name="MyTextBox">Some text</TextBox>

For speed reasons, I want to call the method .AppendText, e.g. In C# code behind, I would call MyTextBox.AppendText("...")

However, this is not very MVVM like. If I want to make a call to a function on a control using binding to my ViewModel, what is an elegant way to achieve this?

I'm using MVVM Light.

Update

I would use the answer from @XAML Lover if I wanted a simple, quick solution. This answer uses a Blend Behavior which is less C# coding.

I would use the answer from @Chris Eelmaa if I wanted write a reusable Dependency Property which I could apply to any TextBox in the future. This example is based on a Dependency Property which, while slightly more complex, is very powerful and reusable once it is written. As it plugs into the native type, there is also slightly less XAML to use it.

like image 411
Contango Avatar asked Mar 18 '23 09:03

Contango


2 Answers

Basically when you call a method from a control, it is obvious that you are doing some UI related logic. And that should not sit in ViewModel. But in some exceptional case, I would suggest to create a behavior. Create a Behavior and define a DependencyProperty of type Action<string> since AppendText should take string as a parameter.

public class AppendTextBehavior : Behavior<TextBlock>
{
    public Action<string> AppendTextAction
    {
        get { return (Action<string>)GetValue(AppendTextActionProperty); }
        set { SetValue(AppendTextActionProperty, value); }
    }

    // Using a DependencyProperty as the backing store for AppendTextAction.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AppendTextActionProperty =
        DependencyProperty.Register("AppendTextAction", typeof(Action<string>), typeof(AppendTextBehavior), new PropertyMetadata(null));

    protected override void OnAttached()
    {
        SetCurrentValue(AppendTextActionProperty, (Action<string>)AssociatedObject.AppendText);
        base.OnAttached();
    }
}

In the OnAttached method, I have assigned the extension method that I have created on TextBlock to the DP of Behavior. Now we can attach this behavior to a TextBlock in View.

    <TextBlock Text="Original String"
               VerticalAlignment="Top">
        <i:Interaction.Behaviors>
            <wpfApplication1:AppendTextBehavior AppendTextAction="{Binding AppendTextAction, Mode=OneWayToSource}" />
        </i:Interaction.Behaviors>
    </TextBlock>

Consider we have a property in ViewModel with same signature. And that property is the source of this binding. Then we can invoke that Action anytime, which will automatically invoke our extension method on TextBlock. Here I am invoking the method on a button click. Remember in this case, our Behavior acts like an Adapter between View and ViewModel.

public class ViewModel
{
    public Action<string> AppendTextAction { get; set; }

    public ICommand ClickCommand { get; set; }

    public ViewModel()
    {
        ClickCommand = new DelegateCommand(OnClick);
    }

    private void OnClick()
    {
        AppendTextAction.Invoke(" test");
    }
}
like image 58
Jawahar Avatar answered Mar 19 '23 23:03

Jawahar


Seems like a reasonable request to me. AppendText is definitely very fast, as it deals with pointers. Pretty much every answer in MVVM world be either subclassing, or attached properties.

You can create new interface, call it ITextBuffer:

public interface ITextBuffer
{
    void Delete();
    void Delete(int offset, int length);

    void Append(string content);
    void Append(string content, int offset);

    string GetCurrentValue();

    event EventHandler<string> BufferAppendedHandler;
}

internal class MyTextBuffer : ITextBuffer
{
    #region Implementation of ITextBuffer

    private readonly StringBuilder _buffer = new StringBuilder();

    public void Delete()
    {
        _buffer.Clear();
    }

    public void Delete(int offset, int length)
    {
        _buffer.Remove(offset, length);
    }

    public void Append(string content)
    {
        _buffer.Append(content);

        var @event = BufferAppendedHandler;
        if (@event != null)
            @event(this, content);
    }

    public void Append(string content, int offset)
    {
        if (offset == _buffer.Length)
        {
            _buffer.Append(content);
        }
        else
        {
            _buffer.Insert(offset, content);
        }
    }

    public string GetCurrentValue()
    {
        return _buffer.ToString();
    }

    public event EventHandler<string> BufferAppendedHandler;

    #endregion
}

This will be used throughout the viewmodels. All you have to do now, is write an attached property that takes advance of such interface, when you do bindings.

Something like this:

public sealed class MvvmTextBox
{
    public static readonly DependencyProperty BufferProperty =
        DependencyProperty.RegisterAttached(
            "Buffer",
            typeof (ITextBuffer),
            typeof (MvvmTextBox),
            new UIPropertyMetadata(null, PropertyChangedCallback)
        );

    private static void PropertyChangedCallback(
        DependencyObject dependencyObject,
        DependencyPropertyChangedEventArgs depPropChangedEvArgs)
    {
        // todo: unrelease old buffer.
        var textBox = (TextBox) dependencyObject;
        var textBuffer = (ITextBuffer) depPropChangedEvArgs.NewValue;

        var detectChanges = true;

        textBox.Text = textBuffer.GetCurrentValue();
        textBuffer.BufferAppendedHandler += (sender, appendedText) =>
        {
            detectChanges = false;
            textBox.AppendText(appendedText);
            detectChanges = true;
        };

        // todo unrelease event handlers.
        textBox.TextChanged += (sender, args) =>
        {
            if (!detectChanges)
                return;

            foreach (var change in args.Changes)
            {
                if (change.AddedLength > 0)
                {
                    var addedContent = textBox.Text.Substring(
                        change.Offset, change.AddedLength);

                    textBuffer.Append(addedContent, change.Offset);
                }
                else
                {
                    textBuffer.Delete(change.Offset, change.RemovedLength);
                }
            }

            Debug.WriteLine(textBuffer.GetCurrentValue());
        };
    }

    public static void SetBuffer(UIElement element, Boolean value)
    {
        element.SetValue(BufferProperty, value);
    }
    public static ITextBuffer GetBuffer(UIElement element)
    {
        return (ITextBuffer)element.GetValue(BufferProperty);
    }
}

The idea here is to wrap StringBuilder into an interface (as it raises no events by default :) which can then be exploited by an attached property & TextBox actual implementation.

In your viewmodel, you'd probably want something like this:

public class MyViewModel
{
    public ITextBuffer Description { get; set; }

    public MyViewModel()
    {
        Description= new MyTextBuffer();

        Description.Append("Just testing out.");
    }
}

and in the view:

<TextBox wpfApplication2:MvvmTextBox.Buffer="{Binding Description}" />
like image 40
Erti-Chris Eelmaa Avatar answered Mar 19 '23 23:03

Erti-Chris Eelmaa