c# - Enlace bidireccional al texto del documento AvalonEdit usando MVVM
wpf binding (4)
Quiero incluir un control AvalonEdit TextEditor
en mi aplicación MVVM. Lo primero que necesito es poder enlazar a la propiedad TextEditor.Text
para que pueda mostrar texto. Para hacer esto he seguido y el ejemplo que se dio en Making AvalonEdit MVVM es compatible . Ahora, he implementado la siguiente clase usando la respuesta aceptada como plantilla
public sealed class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.Text = (string)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Donde el XAML es
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
En primer lugar, esto no funciona. El enlace no se muestra en Snoop en absoluto (no rojo, nada, de hecho ni siquiera puedo ver la propiedad de dependencia de Text
).
He visto esta pregunta que es exactamente igual a la mía. La vinculación bidireccional en AvalonEdit no funciona, pero la respuesta aceptada no funciona (al menos para mí). Entonces mi pregunta es:
¿Cómo puedo realizar un enlace bidireccional utilizando el método anterior y cuál es la implementación correcta de mi clase MvvmTextEditor
?
Gracias por tu tiempo.
Nota: Tengo mi propiedad Text
en mi ViewModel e implementa la interfaz requerida INotifyPropertyChanged
.
Cree una clase de comportamiento que adjuntará el evento TextChanged y enlazará la propiedad de dependencia que está vinculada a ViewModel.
AvalonTextBehavior.cs
public sealed class AvalonEditBehaviour : Behavior<TextEditor>
{
public static readonly DependencyProperty GiveMeTheTextProperty =
DependencyProperty.Register("GiveMeTheText", typeof(string), typeof(AvalonEditBehaviour),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PropertyChangedCallback));
public string GiveMeTheText
{
get { return (string)GetValue(GiveMeTheTextProperty); }
set { SetValue(GiveMeTheTextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
AssociatedObject.TextChanged += AssociatedObjectOnTextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
AssociatedObject.TextChanged -= AssociatedObjectOnTextChanged;
}
private void AssociatedObjectOnTextChanged(object sender, EventArgs eventArgs)
{
var textEditor = sender as TextEditor;
if (textEditor != null)
{
if (textEditor.Document != null)
GiveMeTheText = textEditor.Document.Text;
}
}
private static void PropertyChangedCallback(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var behavior = dependencyObject as AvalonEditBehaviour;
if (behavior.AssociatedObject!= null)
{
var editor = behavior.AssociatedObject as TextEditor;
if (editor.Document != null)
{
var caretOffset = editor.CaretOffset;
editor.Document.Text = dependencyPropertyChangedEventArgs.NewValue.ToString();
editor.CaretOffset = caretOffset;
}
}
}
}
View.xaml
<avalonedit:TextEditor
WordWrap="True"
ShowLineNumbers="True"
LineNumbersForeground="Magenta"
x:Name="textEditor"
FontFamily="Consolas"
SyntaxHighlighting="XML"
FontSize="10pt">
<i:Interaction.Behaviors>
<controls:AvalonEditBehaviour GiveMeTheText="{Binding Test, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</avalonedit:TextEditor>
donde i
se define como "xmlns: i =" clr-namespace: System.Windows.Interactivity; assembly = System.Windows.Interactivity ""
ViewModel.cs
private string _test;
public string Test
{
get { return _test; }
set { _test = value; }
}
Eso debería proporcionarle el texto y enviarlo nuevamente al ViewModel.
No me gustan ninguna de estas soluciones. La razón por la que el autor no creó una propiedad de dependencia en el texto es por razones de rendimiento. Al trabajar a su alrededor creando una propiedad adjunta, se debe recrear la cadena de texto en cada golpe de tecla. En un archivo de 100mb, esto puede ser un problema de rendimiento serio. Internamente, solo utiliza un búfer de documentos y nunca creará la cadena completa a menos que se solicite.
Expone otra propiedad, Documento, que es una propiedad de dependencia, y expone la propiedad Texto para construir la cadena solo cuando sea necesario. Aunque puede vincularse a él, significaría diseñar su ViewModel alrededor de un elemento de UI que anula el propósito de tener un UM agnóstico de ViewModel. No me gusta esa opción tampoco.
Honestamente, la solución más limpia (ish) es crear 2 eventos en su ViewModel, uno para mostrar el texto y otro para actualizar el texto. Luego, escribe un controlador de eventos de una línea en su código subyacente, lo cual está bien, ya que está relacionado exclusivamente con la interfaz de usuario. De esta forma, construyes y asignas la cadena de documentos completa solo cuando es realmente necesaria. Además, ni siquiera necesita almacenar (ni actualizar) el texto en ViewModel. Simplemente levante DisplayScript y UpdateScript cuando sea necesario.
No es una solución ideal, pero hay menos inconvenientes que cualquier otro método que haya visto.
TextBox también se enfrenta a un problema similar, y lo resuelve internamente utilizando un objeto DeferredReference que construye la cadena solo cuando realmente se necesita. Esa clase es interna y no está disponible para el público, y el código de Encuadernación está codificado para manejar DeferredReference de una manera especial. Lamentablemente, no parece haber ninguna forma de resolver el problema de la misma manera que TextBox, quizás a menos que TextEditor herede de TextBox.
Otro buen enfoque de OOP es descargar el código fuente de AvalonEdit (es de origen abierto) y crear una nueva clase que hereda de la clase TextEditor
(el editor principal de AvalonEdit).
Lo que quiere hacer es básicamente anular la propiedad Text
e implementar una versión INotifyPropertyChanged
de ella, usando la propiedad de dependencia para la propiedad Text
y elevando el evento OnPropertyChanged
cuando se cambia el texto (esto puede hacerse anulando el método OnTextChanged()
.
Aquí hay un ejemplo de código rápido (completamente funcional) que funciona para mí:
public class BindableTextEditor : TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(BindableTextEditor), new PropertyMetadata((obj, args) =>
{
var target = (BindableTextEditor)obj;
target.Text = (string)args.NewValue;
}));
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Para aquellos que se preguntan sobre una implementación de MVVM usando AvalonEdit, esta es una de las maneras en que se puede hacer, primero tenemos la clase
/// <summary>
/// Class that inherits from the AvalonEdit TextEditor control to
/// enable MVVM interaction.
/// </summary>
public class CodeEditor : TextEditor, INotifyPropertyChanged
{
// Vars.
private static bool canScroll = true;
/// <summary>
/// Default constructor to set up event handlers.
/// </summary>
public CodeEditor()
{
// Default options.
FontSize = 12;
FontFamily = new FontFamily("Consolas");
Options = new TextEditorOptions
{
IndentationSize = 3,
ConvertTabsToSpaces = true
};
}
#region Text.
/// <summary>
/// Dependancy property for the editor text property binding.
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
target.Text = (string)args.NewValue;
}));
/// <summary>
/// Provide access to the Text.
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// Return the current text length.
/// </summary>
public int Length
{
get { return base.Text.Length; }
}
/// <summary>
/// Override of OnTextChanged event.
/// </summary>
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
/// <summary>
/// Event handler to update properties based upon the selection changed event.
/// </summary>
void TextArea_SelectionChanged(object sender, EventArgs e)
{
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
}
/// <summary>
/// Event that handles when the caret changes.
/// </summary>
void TextArea_CaretPositionChanged(object sender, EventArgs e)
{
try
{
canScroll = false;
this.TextLocation = TextLocation;
}
finally
{
canScroll = true;
}
}
#endregion // Text.
#region Caret Offset.
/// <summary>
/// DependencyProperty for the TextEditorCaretOffset binding.
/// </summary>
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.CaretOffset != (int)args.NewValue)
target.CaretOffset = (int)args.NewValue;
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int CaretOffset
{
get { return base.CaretOffset; }
set { SetValue(CaretOffsetProperty, value); }
}
#endregion // Caret Offset.
#region Selection.
/// <summary>
/// DependencyProperty for the TextLocation. Setting this value
/// will scroll the TextEditor to the desired TextLocation.
/// </summary>
public static readonly DependencyProperty TextLocationProperty =
DependencyProperty.Register("TextLocation", typeof(TextLocation), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
TextLocation loc = (TextLocation)args.NewValue;
if (canScroll)
target.ScrollTo(loc.Line, loc.Column);
}));
/// <summary>
/// Get or set the TextLocation. Setting will scroll to that location.
/// </summary>
public TextLocation TextLocation
{
get { return base.Document.GetLocation(SelectionStart); }
set { SetValue(TextLocationProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionLength property.
/// </summary>
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionLength != (int)args.NewValue)
{
target.SelectionLength = (int)args.NewValue;
target.Select(target.SelectionStart, (int)args.NewValue);
}
}));
/// <summary>
/// Access to the SelectionLength property.
/// </summary>
public new int SelectionLength
{
get { return base.SelectionLength; }
set { SetValue(SelectionLengthProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionStart property.
/// </summary>
public static readonly DependencyProperty SelectionStartProperty =
DependencyProperty.Register("SelectionStart", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionStart != (int)args.NewValue)
{
target.SelectionStart = (int)args.NewValue;
target.Select((int)args.NewValue, target.SelectionLength);
}
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int SelectionStart
{
get { return base.SelectionStart; }
set { SetValue(SelectionStartProperty, value); }
}
#endregion // Selection.
#region Properties.
/// <summary>
/// The currently loaded file name. This is bound to the ViewModel
/// consuming the editor control.
/// </summary>
public string FilePath
{
get { return (string)GetValue(FilePathProperty); }
set { SetValue(FilePathProperty, value); }
}
// Using a DependencyProperty as the backing store for FilePath.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty FilePathProperty =
DependencyProperty.Register("FilePath", typeof(string), typeof(CodeEditor),
new PropertyMetadata(String.Empty, OnFilePathChanged));
#endregion // Properties.
#region Raise Property Changed.
/// <summary>
/// Implement the INotifyPropertyChanged event handler.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string caller = null)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
#endregion // Raise Property Changed.
}
Luego, en su punto de vista donde desea tener AvalonEdit, puede hacer
...
<Grid>
<Local:CodeEditor
x:Name="CodeEditor"
FilePath="{Binding FilePath,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
WordWrap="{Binding WordWrap,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
ShowLineNumbers="{Binding ShowLineNumbers,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionLength="{Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionStart="{Binding SelectionStart,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
TextLocation="{Binding TextLocation,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid>
Donde esto se puede colocar en un UserControl o Ventana o lo que sea, entonces en el ViewModel para esta vista tenemos (donde estoy usando Caliburn Micro para las cosas del framework MVVM)
public string FilePath
{
get { return filePath; }
set
{
if (filePath == value)
return;
filePath = value;
NotifyOfPropertyChange(() => FilePath);
}
}
/// <summary>
/// Should wrap?
/// </summary>
public bool WordWrap
{
get { return wordWrap; }
set
{
if (wordWrap == value)
return;
wordWrap = value;
NotifyOfPropertyChange(() => WordWrap);
}
}
/// <summary>
/// Display line numbers?
/// </summary>
public bool ShowLineNumbers
{
get { return showLineNumbers; }
set
{
if (showLineNumbers == value)
return;
showLineNumbers = value;
NotifyOfPropertyChange(() => ShowLineNumbers);
}
}
/// <summary>
/// Hold the start of the currently selected text.
/// </summary>
private int selectionStart = 0;
public int SelectionStart
{
get { return selectionStart; }
set
{
selectionStart = value;
NotifyOfPropertyChange(() => SelectionStart);
}
}
/// <summary>
/// Hold the selection length of the currently selected text.
/// </summary>
private int selectionLength = 0;
public int SelectionLength
{
get { return selectionLength; }
set
{
selectionLength = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => SelectionLength);
}
}
/// <summary>
/// Gets or sets the TextLocation of the current editor control. If the
/// user is setting this value it will scroll the TextLocation into view.
/// </summary>
private TextLocation textLocation = new TextLocation(0, 0);
public TextLocation TextLocation
{
get { return textLocation; }
set
{
textLocation = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => TextLocation);
}
}
¡Y eso es! Hecho.
Espero que esto ayude.
Editar. para todos aquellos que buscan un ejemplo de trabajo con AvalonEdit usando MVVM, puede descargar una aplicación de editor muy básica de http://1drv.ms/1E5nhCJ.
Notas. Esta aplicación realmente crea un control de editor amigable MVVM heredando del control estándar AvalonEdit y le agrega Propiedades de dependencia adicionales según corresponda - * esto es diferente a lo que he mostrado en la respuesta dada anteriormente *. Sin embargo, en la solución también he mostrado cómo se puede hacer (como lo describo en la respuesta anterior) usando Propiedades adjuntas y hay un código en la solución en el espacio de nombres Behaviors
. Sin embargo, lo que realmente se implementa es el primero de los enfoques anteriores.
Tenga en cuenta también que hay algún código en la solución que no se utiliza. Esta * muestra * era una versión reducida de una aplicación más grande y he dejado algún código, ya que podría ser útil para el usuario que descarga este editor de ejemplo. Además de lo anterior, en el código de ejemplo, accedo al Texto al encuadernarlo al documento, hay algunos más puros que pueden argumentar que esto no es puro-MVVM, y digo "está bien, pero funciona". Algunas veces luchar contra este patrón no es el camino a seguir.
Espero que esto sea útil para algunos de ustedes.