MVVM:如何对控件进行函数调用?
MVVM: How to make a function call on a control?
在 XAML 中,我有一个 x:Name 为 MyTextBox
的文本框。
<TextBox x:Name="MyTextBox">Some text</TextBox>
出于速度原因,我想调用方法 .AppendText
,例如在后面的 C# 代码中,我会调用 MyTextBox.AppendText("...")
但是,这不是很像 MVVM。如果我想使用绑定到我的 ViewModel 来调用控件上的函数,实现此目的的优雅方法是什么?
我正在使用 MVVM Light。
更新
如果我想要一个简单、快速的解决方案,我会使用来自@XAML Lover 的答案。此答案使用较少 C# 编码的混合行为。
如果我想编写一个可重复使用的依赖项 属性,我会使用@Chris Eelmaa 的答案,我可以在将来将其应用于任何 TextBox。这个例子是基于一个 Dependency 属性 的,它虽然稍微复杂一点,但是非常强大并且一旦编写就可以重用。由于它插入原生类型,因此使用它的人也略少XAML。
对我来说似乎是一个合理的要求。 AppendText
肯定非常快,因为它处理指针。几乎 MVVM 世界中的每个答案都是子类化或附加属性。
你可以新建一个界面,命名为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
}
这将在整个视图模型中使用。您现在要做的就是编写一个附加的 属性,在您进行绑定时使用此类接口。
像这样:
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);
}
}
这里的想法是将 StringBuilder
包装到一个接口中(因为默认情况下它不引发任何事件:) 然后可以被附加的 属性 & TextBox
实际实现利用.
在您的视图模型中,您可能需要这样的东西:
public class MyViewModel
{
public ITextBuffer Description { get; set; }
public MyViewModel()
{
Description= new MyTextBuffer();
Description.Append("Just testing out.");
}
}
并且在视图中:
<TextBox wpfApplication2:MvvmTextBox.Buffer="{Binding Description}" />
基本上当你从一个控件中调用一个方法时,很明显你在做一些UI相关的逻辑。那不应该放在 ViewModel 中。但在某些特殊情况下,我会建议创建一个行为。创建一个 Behavior 并定义一个类型为 Action 的 DependencyProperty,因为 AppendText 应该将字符串作为参数。
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();
}
}
在OnAttached 方法中,我将我在TextBlock 上创建的扩展方法分配给了Behavior 的DP。现在我们可以将此行为附加到 View 中的 TextBlock。
<TextBlock Text="Original String"
VerticalAlignment="Top">
<i:Interaction.Behaviors>
<wpfApplication1:AppendTextBehavior AppendTextAction="{Binding AppendTextAction, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</TextBlock>
假设我们在 ViewModel 中有一个 属性,具有 相同的签名 。而那个 属性 就是这个绑定的来源。然后我们可以随时调用该 Action,这将自动调用我们在 TextBlock 上的扩展方法。在这里,我在单击按钮时调用该方法。请记住,在这种情况下,我们的行为就像 View 和 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");
}
}
在 XAML 中,我有一个 x:Name 为 MyTextBox
的文本框。
<TextBox x:Name="MyTextBox">Some text</TextBox>
出于速度原因,我想调用方法 .AppendText
,例如在后面的 C# 代码中,我会调用 MyTextBox.AppendText("...")
但是,这不是很像 MVVM。如果我想使用绑定到我的 ViewModel 来调用控件上的函数,实现此目的的优雅方法是什么?
我正在使用 MVVM Light。
更新
如果我想要一个简单、快速的解决方案,我会使用来自@XAML Lover 的答案。此答案使用较少 C# 编码的混合行为。
如果我想编写一个可重复使用的依赖项 属性,我会使用@Chris Eelmaa 的答案,我可以在将来将其应用于任何 TextBox。这个例子是基于一个 Dependency 属性 的,它虽然稍微复杂一点,但是非常强大并且一旦编写就可以重用。由于它插入原生类型,因此使用它的人也略少XAML。
对我来说似乎是一个合理的要求。 AppendText
肯定非常快,因为它处理指针。几乎 MVVM 世界中的每个答案都是子类化或附加属性。
你可以新建一个界面,命名为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
}
这将在整个视图模型中使用。您现在要做的就是编写一个附加的 属性,在您进行绑定时使用此类接口。
像这样:
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);
}
}
这里的想法是将 StringBuilder
包装到一个接口中(因为默认情况下它不引发任何事件:) 然后可以被附加的 属性 & TextBox
实际实现利用.
在您的视图模型中,您可能需要这样的东西:
public class MyViewModel
{
public ITextBuffer Description { get; set; }
public MyViewModel()
{
Description= new MyTextBuffer();
Description.Append("Just testing out.");
}
}
并且在视图中:
<TextBox wpfApplication2:MvvmTextBox.Buffer="{Binding Description}" />
基本上当你从一个控件中调用一个方法时,很明显你在做一些UI相关的逻辑。那不应该放在 ViewModel 中。但在某些特殊情况下,我会建议创建一个行为。创建一个 Behavior 并定义一个类型为 Action
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();
}
}
在OnAttached 方法中,我将我在TextBlock 上创建的扩展方法分配给了Behavior 的DP。现在我们可以将此行为附加到 View 中的 TextBlock。
<TextBlock Text="Original String"
VerticalAlignment="Top">
<i:Interaction.Behaviors>
<wpfApplication1:AppendTextBehavior AppendTextAction="{Binding AppendTextAction, Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</TextBlock>
假设我们在 ViewModel 中有一个 属性,具有 相同的签名 。而那个 属性 就是这个绑定的来源。然后我们可以随时调用该 Action,这将自动调用我们在 TextBlock 上的扩展方法。在这里,我在单击按钮时调用该方法。请记住,在这种情况下,我们的行为就像 View 和 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");
}
}