I have :
MainWindow.xaml (where I have the frame)
LoginPage.xaml
SignUpPage.xaml
Here is the frame in MainWindow.xaml:
<Frame x:Name="MainPage"
Content="{Binding ApplicationViewModel.CurentPage,
Source={x:Static viewMod:ViewModelLocator.Instanze},
Converter={local:ApplicationPageValueConverter}}"/>
ApplicationViewModel is the application state as a view model :
public class ApplicationViewModel
{
/// <summary>
/// The current page of the application
/// </summary>
public ApplicationPage CurentPage { get; set; } = ApplicationPage.Login;
}
ViewModelLocator locates view models from the IoC for use in binding in Xaml files
public class ViewModelLocator
{
/// <summary>
/// Singleton instance of the locator
/// </summary>
public static ViewModelLocator Instance { get; private set; } = new ViewModelLocator();
/// <summary>
/// The application view model
/// </summary>
public static ApplicationViewModel ApplicationViewModel => IoC.Get<ApplicationViewModel>();
}
In ApplicationPageValueConverter I have this, to convert the page:
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch ((ApplicationPage)value)
{
case ApplicationPage.Login:
return new LoginPage();
case ApplicationPage.SignUp:
return new SignUpPage();
default:
Debugger.Break();
return null;
}
}
In the MainViewModel which is ViewModel for MainWindow.xaml.cs I have a button "SignUp", and when I click the button is going to execute ICommand whose is doing this:
public ICommand LoginCommand { get; set; }
LoginCommand = new RelayCommand(() => Login());
private void Login()
{
IoC.Get<ApplicationViewModel>().CurentPage = ApplicationPage.SignUp;
}
The value of ApplicationViewModel.CurentPage is changed to ApplicationPage.SignUp but it doesn't go to ApplicationPageValueConverter to convert/show the page.
Here is the IoC code where OnStartup I'm doing this :
base.OnStartup(e);
IoC.SetUp();
....
I can't get whay it doesn't show the page, what I'm doing wrong?
ApplicationViewModel should implement INotifyPropertyChanged and raise the PropertyChanged event whenever the CurrentPage property is set:
public class ApplicationViewModel : INotifyPropertyChanged
{
private ApplicationPage _currentPage = ApplicationPage.Login;
/// <summary>
/// The current page of the application
/// </summary>
public ApplicationPage CurentPage
{
get { return _currentPage; }
set { _currentPage = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
This is required for the view to be notified of the change and the converter to get invoked again.
This question already has answers here:
WPF MVVM command canexecute enable/disable button
(4 answers)
Disable button in WPF?
(5 answers)
How does one "disable" a button in WPF using the MVVM pattern?
(5 answers)
Command source disabling and enabling
(1 answer)
Closed 5 years ago.
I have the command
public class RelayActionCommand : ICommand
{
/// <summary>
/// The Action Delegate representing a method with input parameter
/// </summary>
public Action<object> ExecuteAction { get; set; }
/// <summary>
/// The Delegate, used to represent the method which defines criteria for the execution
/// </summary>
public Predicate<object> CanExecuteAction { get; set; }
public bool CanExecute(object parameter)
{
if (CanExecuteAction != null)
{
return CanExecuteAction(parameter);
}
return true;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
if (ExecuteAction != null)
{
ExecuteAction(parameter);
}
}
}
To use it,
public RelayActionCommand SearchPersonCommnad { get; set; }
DataAccess objds;
public PersonViewModel()
{
Persons = new ObservableCollection<PersonInfo>();
objds = new DataAccess();
Persons = new ObservableCollection<PersonInfo>(objds.GetPersonData());
var defaultView = CollectionViewSource.GetDefaultView(Persons);
//based upon the data entered in the TextBox
SearchPersonCommnad = new RelayActionCommand()
{
CanExecuteAction = n=> !String.IsNullOrEmpty(Name),
ExecuteAction = n => defaultView.Filter = name => ((PersonInfo)name).FirstName.StartsWith(Name)
|| ((PersonInfo)name).LastName.StartsWith(Name)
|| ((PersonInfo)name).City==Name
};
At the beginning, the button is disabled. But in running time, it changes by different situations. My question is how to set up the button's IsEnabled property with it? Which means, when ExecuteAction I have to set up the property correctly.
UPDATE:
I use ICommand not DelegateCommand.
You can use the CanExecute method, but it is good practice is actually to avoid this, and bind the button's enabled state to a separate boolean property of the view model. Most other solutions will have unexpected effects, or be suboptimal. Why?
CanExecute is a method. This means that it needs to be polled for the button state to change. You can force the control that's using the command to re-poll on a status change, but the code is much cleaner and more straightforward if you just use a property on the view model. This is because as a method, you can't use INotifyPropertyChanged to notify for changes, whereas with a property you can.
The danger in using CanExecute is that the user will manage to click the button after the method would return false, but before the button's enablement has changed.
Edit: Code to do what you want:
public class ViewModel : INotifyPropertyChanged
{
private int someValue;
private bool isEnabled;
public ViewModel()
{
MyCommand = new RelayActionCommand(Click);
}
private void Click(object obj)
{
//Do something.
}
/// <summary>
/// Bind this to the IsEnabled property of the button, and
/// also the background using a convertor or see ButtonBackground.
/// </summary>
public bool IsEnabled => SomeValue < 5;
/// <summary>
/// Option 2 - use this property to bind to the background of the button.
/// </summary>
public Brush ButtonBackground => IsEnabled ? Brushes.SeaShell : Brushes.AntiqueWhite;
public int SomeValue
{
get { return someValue; }
set
{
if (value == someValue) return;
someValue = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(ButtonBackground));
}
}
/// <summary>
/// Bind this to the command of the button.
/// </summary>
public RelayActionCommand MyCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Relay command fixed up a bit to avoid using CanExecute:
public class RelayActionCommand : ICommand
{
public RelayActionCommand(Action<object> executeAction)
{
ExecuteAction = executeAction;
}
/// <summary>
/// The Action Delegate representing a method with input parameter
/// </summary>
public Action<object> ExecuteAction { get; }
/// <summary>
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
ExecuteAction?.Invoke(parameter);
}
//Deliberately empty.
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
EDIT 2: Code to do what you want using a DelegateCommand
Note, this does not use InvalidateRequerySuggested - mainly because it refreshes all buttons when any CanExecute changes, which is a poor solution. As you can see, this is less immediately straightforward than putting the code in the view model directly, but whatever floats your boat I guess.
public sealed class ViewModel : INotifyPropertyChanged
{
private int calls;
public ViewModel()
{
SafeOnceCommand = new RelayCommand(DoItOnce, HasDoneIt);
}
private bool HasDoneIt()
{
return Calls == 0;
}
private void DoItOnce()
{
if (Calls > 0) throw new InvalidOperationException();
Calls++;
}
public int Calls
{
get { return calls; }
set
{
if (value == calls) return;
calls = value;
OnPropertyChanged();
SafeOnceCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand SafeOnceCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed class RelayCommand : ICommand
{
private readonly Action execute;
private readonly Func<bool> canExecute;
private readonly List<EventHandler> invocationList = new List<EventHandler>();
public RelayCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute();
}
public void Execute(object parameter)
{
execute();
}
/// <summary>
/// Method to raise CanExecuteChanged event
/// </summary>
public void RaiseCanExecuteChanged()
{
foreach (var elem in invocationList)
{
elem(null, EventArgs.Empty);
}
}
public event EventHandler CanExecuteChanged
{
add { invocationList.Add(value); }
remove { invocationList.Remove(value); }
}
}
I want to include an AvalonEdit TextEditor control into my MVVM application. The first thing I require is to be able to bind to the TextEditor.Text property so that I can display text. To do this I have followed and example that was given in Making AvalonEdit MVVM compatible. Now, I have implemented the following class using the accepted answer as a template
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));
}
}
Where the XAML is
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
Firstly, this does not work. The Binding is not shown in Snoop at all (not red, not anything, in fact I cannot even see the Text dependency property).
I have seen this question which is exactly the same as mine Two-way binding in AvalonEdit doesn't work but the accepted answer does not work (at least for me). So my question is:
How can I perform two way binding using the above method and what is the correct implementation of my MvvmTextEditor class?
Thanks for your time.
Note: I have my Text property in my ViewModel and it implements the required INotifyPropertyChanged interface.
Create a Behavior class that will attach the TextChanged event and will hook up the dependency property that is bound to the 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>
i must be defined as
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; }
}
That should give you the Text and push it back to the ViewModel.
Create a BindableAvalonEditor class with a two-way binding on the Text property.
I was able to establish a two-way binding with the latest version of AvalonEdit by combining Jonathan Perry's answer and 123 456 789 0's answer. This allows a direct two-way binding without the need for behaviors.
Here is the source code...
public class BindableAvalonEditor : ICSharpCode.AvalonEdit.TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
RaisePropertyChanged("Text");
}
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(BindableAvalonEditor),
new FrameworkPropertyMetadata
{
DefaultValue = default(string),
BindsTwoWayByDefault = true,
PropertyChangedCallback = OnDependencyPropertyChanged
}
);
protected static void OnDependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = (BindableAvalonEditor)obj;
if (target.Document != null)
{
var caretOffset = target.CaretOffset;
var newValue = args.NewValue;
if (newValue == null)
{
newValue = "";
}
target.Document.Text = (string)newValue;
target.CaretOffset = Math.Min(caretOffset, newValue.ToString().Length);
}
}
protected override void OnTextChanged(EventArgs e)
{
if (this.Document != null)
{
Text = this.Document.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;
}
I like none of these solutions. The reason the author didn't create a dependency property on Text is for performance reason. Working around it by creating an attached property means the text string must be recreated on every key stroke. On a 100mb file, this can be a serious performance issue. Internally, it only uses a document buffer and will never create the full string unless requested.
It exposes another property, Document, which is a dependency property, and it exposes the Text property to construct the string only when needed. Although you can bind to it, it would mean designing your ViewModel around a UI element which defeats the purpose of having a ViewModel UI-agnostic. I don't like that option either.
Honestly, the cleanest(ish) solution is to create 2 events in your ViewModel, one to display the text and one to update the text. Then you write a one-line event handler in your code-behind, which is fine since it's purely UI-related. That way, you construct and assign the full document string only when it's truly needed. Additionally, you don't even need to store (nor update) the text in the ViewModel. Just raise DisplayScript and UpdateScript when it is needed.
It's not an ideal solution, but there are less drawbacks than any other method I've seen.
TextBox also faces a similar issue, and it solves it by internally using a DeferredReference object that constructs the string only when it is really needed. That class is internal and not available to the public, and the Binding code is hard-coded to handle DeferredReference in a special way. Unfortunately there doesn't seen to be any way of solving the problem in the same way as TextBox -- perhaps unless TextEditor would inherit from TextBox.
Another nice OOP approach is to download the source code of AvalonEdit (it's open sourced), and creating a new class that inherits from TextEditor class (the main editor of AvalonEdit).
What you want to do is basically override the Text property and implement an INotifyPropertyChanged version of it, using dependency property for the Text property and raising the OnPropertyChanged event when text is changed (this can be done by overriding the OnTextChanged() method.
Here's a quick code (fully working) example that works for me:
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;
}
For those wondering about an MVVM implementation using AvalonEdit, here is one of the ways it can be done, first we have the class
/// <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.
}
Then in your view where you want to have AvalonEdit, you can do
...
<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>
Where this can be placed in a UserControl or Window or what ever, then in the ViewModel for this view we have (where I am using Caliburn Micro for the MVVM framework stuff)
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);
}
}
And that's it! Done.
I hope this helps.
Edit. for all those looking for an example of working with AvalonEdit using MVVM, you can download a very basic editor application from http://1drv.ms/1E5nhCJ.
Notes. This application actually creates a MVVM friendly editor control by inheriting from the AvalonEdit standard control and adds additional Dependency Properties to it as appropriate - *this is different to what I have shown in the answer given above*. However, in the solution I have also shown how this can be done (as I describe in the answer above) using Attached Properties and there is code in the solution under the Behaviors namespace. What is actually implemented however, is the first of the above approaches.
Please also be aware that there is some code in the solution that is unused. This *sample* was a stripped back version of a larger application and I have left some code in as it could be useful to the user who downloads this example editor. In addition to the above, in the example code I access the Text by binding to document, there are some purest that may argue that this is not pure-MVVM, and I say "okay, but it works". Some times fighting this pattern is not the way to go.
I hope this of use to some of you.
I'm currently using Prism's InteractionRequest to display new windows. I use them for simple confirmations as well as displaying a window window with a custom view/viewmodel, following the sample here. Anyway, in all of these cases, I display the window and some button on the window is responsible for closing it. I'd like to display a window and have the object that called it be responsible for closing it.
Here is my implementation:
ActionNotification
public abstract class ActionNotification: Notification, INotifyPropertyChanged, IPopupWindowActionAware
{
public event PropertyChangedEventHandler PropertyChanged;
// IPopupWindowActionAware
public System.Windows.Window HostWindow { get; set; } // Set when the "action" in the view is triggered
public Notification HostNotification { get; set; } // Set when the "action" in the view is triggered
public ActionNotification(string content)
{
this.Content = content;
}
public void CompleteAction()
{
if (this.HostWindow != null)
{
this.HostWindow.Close();
}
}
// INotifyPropertyChange implementation
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Calling method
/// <summary>
/// Pushes a unit of work onto a separate thread and notifies the view to display an action notification
/// </summary>
/// <param name="actionNotification">The notification object for the view to display</param>
/// <param name="act">The unit of work to perform on a separate thread</param>
private void DoWorkAndRaiseAction(ActionNotification actionNotification, Action act)
{
Task.Factory.StartNew(() =>
{
try
{
act();
}
finally
{
Application.Current.Dispatcher.Invoke((Action)(() => actionNotification.CompleteAction()));
}
});
ActionInteractionReq.Raise(actionNotification);
}
This all works well but it appears that I would be suck if the "work" completed before I was able to raise the InteractionRequest. Can anyone offer some advice to GUARANTEE either the work hasn't completed before raising the request otherwise don't raid the request?
EDIT: I should add that the window is being shown as modal, so no code is executed after the request is raised, which is why I push the work off onto a separate task
EDIT2: Here is how the view interacts with the request:
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding Path=ActionInteractionReq, Mode=OneWay}">
<int_req:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" WindowStyle="None" WindowHeight="150" WindowWidth="520">
<int_req:PopupWindowAction.WindowContent>
<int_req:ZActionNotificationView/>
</int_req:PopupWindowAction.WindowContent>
</int_req:PopupWindowAction>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
When Raise is called, the PopupWindowAction is triggered and creates a new Window. It then does ShowDialog on that window.
EDIT3: From the advice from the comments, I've included the PopupWindowAction. I've cut out some irrelevant code for the sake of brevity
public class PopupWindowAction : TriggerAction<FrameworkElement>
{
/*
Here is where a few dependency properties live that dictate things like Window size and other stuff, e.g.
/// <summary>
/// Determines if the content should be shown in a modal window or not.
/// </summary>
public static readonly DependencyProperty IsModalProperty =
DependencyProperty.Register(
"IsModal",
typeof(bool),
typeof(PopupWindowAction),
new PropertyMetadata(null));
*/
/*
Here is where the accessors live for the DPs, e.g.
/// <summary>
/// Gets or sets if the window will be modal or not.
/// </summary>
public bool IsModal
{
get { return (bool)GetValue(IsModalProperty); }
set { SetValue(IsModalProperty, value); }
}
*/
#region PopupWindowAction logic
/// <summary>
/// Displays the child window and collects results for <see cref="IInteractionRequest"/>.
/// </summary>
/// <param name="parameter">The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference.</param>
protected override void Invoke(object parameter)
{
var args = parameter as InteractionRequestedEventArgs;
if (args == null)
{
return;
}
// If the WindowContent shouldn't be part of another visual tree.
if (this.WindowContent != null && this.WindowContent.Parent != null)
{
return;
}
var wrapperWindow = this.GetWindow(args.Context); // args.Context here is the Notification object I'm passing to the InteractionRequest
var callback = args.Callback;
EventHandler handler = null;
handler =
(o, e) =>
{
wrapperWindow.Closed -= handler;
wrapperWindow.Owner = null;
wrapperWindow.Content = null;
callback();
};
wrapperWindow.Closed += handler;
if (this.IsModal)
{
wrapperWindow.ShowDialog();
}
else
{
wrapperWindow.Show();
}
}
/// <summary>
/// Checks if the WindowContent or its DataContext implements IPopupWindowActionAware and IRegionManagerAware.
/// If so, it sets the corresponding values.
/// Also, if WindowContent does not have a RegionManager attached, it creates a new scoped RegionManager for it.
/// </summary>
/// <param name="notification">The notification to be set as a DataContext in the HostWindow.</param>
/// <param name="wrapperWindow">The HostWindow</param>
protected void PrepareContentForWindow(Notification notification, Window wrapperWindow)
{
if (this.WindowContent == null)
{
return;
}
// We set the WindowContent as the content of the window.
wrapperWindow.Content = this.WindowContent;
/* Code removed for brevity */
// If the WindowContent implements IPopupWindowActionAware, we set the corresponding values.
IPopupWindowActionAware popupAwareContent = this.WindowContent as IPopupWindowActionAware;
if (popupAwareContent != null)
{
popupAwareContent.HostWindow = wrapperWindow;
popupAwareContent.HostNotification = notification;
}
// If the WindowContent's DataContext implements IPopupWindowActionAware, we set the corresponding values.
IPopupWindowActionAware popupAwareDataContext = this.WindowContent.DataContext as IPopupWindowActionAware;
if (popupAwareDataContext != null)
{
popupAwareDataContext.HostWindow = wrapperWindow;
popupAwareDataContext.HostNotification = notification;
}
}
#endregion
#region Window creation methods
/// <summary>
/// Returns the window to display as part of the trigger action.
/// </summary>
/// <param name="notification">The notification to be set as a DataContext in the window.</param>
/// <returns></returns>
protected Window GetWindow(Notification notification)
{
Window wrapperWindow;
if (this.WindowContent != null)
{
wrapperWindow = new Window();
wrapperWindow.WindowStyle = this.WindowStyle;
// If the WindowContent does not have its own DataContext, it will inherit this one.
wrapperWindow.DataContext = notification;
wrapperWindow.Title = notification.Title ?? string.Empty;
this.PrepareContentForWindow(notification, wrapperWindow);
}
else
{
wrapperWindow = this.CreateDefaultWindow(notification);
wrapperWindow.DataContext = notification;
}
return wrapperWindow;
}
private Window CreateDefaultWindow(Notification notification)
{
return new DefaultNotificationWindow
{
NotificationTemplate = this.ContentTemplate,
MessageBoxImage = GetImageFromNotification(notification as ZBaseNotification)
};
}
#endregion
}
The underlying issue here is that the code that starts the async operation and the code that displays the window are just not cooperating. The design based on IPopupWindowActionAware is IMHO not very good; pushing property values around is OK for common scenarios, but here it starts showing its limitations.
Let's first consider a localized solution that works with the current code:
public Window HostWindow { /* call OnPropertyChanged! */ }
public void CompleteAction()
{
if (this.HostWindow != null)
{
this.HostWindow.Close();
}
else
{
this.PropertyChanged += (o, e) => {
if (e.PropertyName == "HostWindow" && this.HostWindow != null)
{
var hostWindow = this.HostWindow; // prevent closure-related bugs
// kill it whenever it appears in the future
hostWindow.Loaded += (o, e) => { hostWindow.Close(); };
// kill it right now as well if it's been shown already
// (we cannot assume anything)
if (hostWindow.IsLoaded)
{
hostWindow.Close();
}
}
};
}
}
This is not quite elegant, but it does the job: if CompleteAction is called before the window is known, then when the window becomes known we attach a handler that closes it immediately whenever it get displayed. The double-deep event handler assignment is necessary because the window might not be shown at the time it becomes known to us.