The basic animations in UWP is good and all but I would like to create my own animation. So I looked at the different animations and saw that they all are a subclass of Timeline. Just to test, I decided to do a copy of the DoubleAnimation class like this:
public sealed class MyAnimation : Timeline
{
public static DependencyProperty _ByProperty;
public static DependencyProperty _EasingFunctionProperty;
public static DependencyProperty _EnableDependentAnimationProperty;
public static DependencyProperty _FromProperty;
public static DependencyProperty _ToProperty;
public MyAnimation() : base()
{
}
static MyAnimation()
{
_ByProperty = DependencyProperty.Register("By", typeof(double?), typeof(MyAnimation), new PropertyMetadata((double?)null));
_EasingFunctionProperty = DependencyProperty.Register("EasingFunction", typeof(EasingFunctionBase), typeof(MyAnimation), new PropertyMetadata(null));
_EnableDependentAnimationProperty = DependencyProperty.Register("EnableDependentAnimation", typeof(bool), typeof(MyAnimation), new PropertyMetadata(false));
_FromProperty = DependencyProperty.Register("From", typeof(double?), typeof(MyAnimation), new PropertyMetadata((double?)null));
_ToProperty = DependencyProperty.Register("To", typeof(double?), typeof(MyAnimation), new PropertyMetadata((double?)null));
}
public static DependencyProperty ByProperty { get { return _ByProperty; } }
public static DependencyProperty EasingFunctionProperty { get { return _EasingFunctionProperty; } }
public static DependencyProperty EnableDependentAnimationProperty { get { return _EnableDependentAnimationProperty; } }
public static DependencyProperty FromProperty { get { return _FromProperty; } }
public static DependencyProperty ToProperty { get { return _ToProperty; } }
public double? To { get { return (double?)GetValue(_ToProperty); } set { SetValue(_ToProperty, value); } }
public double? From { get { return (double?)GetValue(_FromProperty); } set { SetValue(_FromProperty, value); } }
public bool EnableDependentAnimation { get { return (bool)GetValue(_EnableDependentAnimationProperty); } set { SetValue(_EnableDependentAnimationProperty, value); } }
public EasingFunctionBase EasingFunction { get { return (EasingFunctionBase)GetValue(_EasingFunctionProperty); } set { SetValue(_EasingFunctionProperty, value); } }
public double? By { get { return (double?)GetValue(_ByProperty); } set { SetValue(_ByProperty, value); } }
}
Then I created an object to move:
<Grid Name="Root" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Ellipse Width="200" Height="200" Fill="Green" Name="MyEllipse"/>
</Grid>
And then I start the animation when everything is loaded:
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Storyboard sb = new Storyboard();
MyAnimation da = new MyAnimation();
da.From = 200;
da.To = 500;
da.Duration = new Duration(TimeSpan.FromSeconds(5));
da.EnableDependentAnimation = true;
sb.Children.Add(da);
Storyboard.SetTargetProperty(da, "Width");
Storyboard.SetTarget(da, MyEllipse);
sb.Begin();
}
Now to my problems. When I run this I get the following exception:
ERROR: System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
at Windows.UI.Xaml.Media.Animation.Storyboard.Begin()
at TestAnimation.MainPage.Button_Click(Object sender, RoutedEventArgs e)
Which gives me zero explanation for what went wrong. I am guessing something more needs to be done inside the constructor but I can not read the source code of the DoubleAnimation, only the metadata, which makes it impossible to know what actually happens. Anyone know what needs to be done in order to get this to work?
Please read the comments on the question because I think they still give a better answer to the question
But, I came across this question as I was also getting a
ERROR: System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component
In my case it had nothing to do with the animation system. It was because I had made a mistake in my scaling algorithm and was returning a desiredSize of new Size(27688, 8) from my override MeasureOverride(...).
From I read and after multiple attempts, UWP does not offer the means to create your own animations as there are no methods to override from Timeline.
My scenario was supposed to be a simple one - how do I animate a Grid using a DoubleAnimation?! Sounds trivial, but a) Grid takes GridLength and not Double, b) one cannot attach an IValueConverter to StoryBoard.TargetProperty, and c) creating your own custom animation seems not to be supported in UWP.
As such, the only option remaining is to leverage from the existing animations and build a layer on top. The most common approach is to resort to two Dependency Properties in the Page, bind one with the DoubleAnimation and another with the Grid. Works, but it is not scalable, especially if you want to decouple your styling.
Here is the outline of my approach:
- Create a global ConverterService that attaches a property "Converter" to any Dependency Object
- Create a generic converter that is an IValueConverter
- The generic value converter has a DependencyProperty called Input of type Tin, and another DependencyProperty called Output of type Tout
- Bind the DoubleAnimation with the Input of your converter and the Output with the Grid.
Here is how it is used:
<Grid Style="{StaticResource MainGrid}" x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ElementName=MyMove, Path=Output}" x:Name="MyColumn">
<conv:ConverterService.Converter>
<conv:DoubleToGridLengthConverter x:Name="MyMove" Input="2" GridUnit="Star" />
</conv:ConverterService.Converter>
</ColumnDefinition>
</Grid.ColumnDefinitions>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Portrait">
<VisualState.StateTriggers>
<stateTriggers:WindowAspectRatioTrigger Orientation="Portrait" />
</VisualState.StateTriggers>
<VisualState.Storyboard>
<Storyboard>
<DoubleAnimation
BeginTime="0:0:0"
EnableDependentAnimation="True"
Storyboard.TargetName ="MyMove"
Storyboard.TargetProperty="Input"
From="1" To="15" Duration="0:0:3" FillBehavior="HoldEnd" />
</Storyboard>
</VisualState.Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
And of course, mind to import the namespaces at the top of your page.
In my case:
<base:BasePage
x:Class="MyProject.MainPage"
x:Name="MyPage"
xmlns:base ="using:MyProject.Base"
xmlns:local="using:MyProject"
xmlns:conv="using:MyProject.Converters"
xmlns:stateTriggers="using:MyProject.StateTriggers"
xmlns:ctrl="using:MyProject.UserControls"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
And here is the code for the ConverterService and BaseConverter and DoubleToGridLengthConverter:
/// <summary>A generic converter.</summary>
public interface IConverter: IValueConverter
{
/// <summary>The input value.</summary>
object Input { get; set; }
/// <summary>The output value.</summary>
object Output { get; set; }
}
/// <summary>A service that provides conversion capabilities to dependency objects via an attached property.</summary>
public abstract class ConverterService : DependencyObject
{
/// <summary>The Converter dependency property.</summary>
public static readonly DependencyProperty ConverterProperty;
/// <summary>The Converters dependency property which is a collection of Converter.</summary>
public static readonly DependencyProperty ConvertersProperty;
static ConverterService()
{
ConverterProperty = DependencyProperty.RegisterAttached("Converter",
typeof(IConverter),
typeof(ConverterService),
new PropertyMetadata(null));
ConvertersProperty = DependencyProperty.RegisterAttached("Converters",
typeof(IList<IConverter>),
typeof(ConverterService),
new PropertyMetadata(new List<IConverter>()));
}
/// <summary>Property getter for attached property Converter.</summary>
/// <param name="element">The dependency object to which the attached property applies.</param>
/// <returns>Returns the converter associated with the specified dependency object.</returns>
public static IConverter GetConverter(DependencyObject element)
{
return (IConverter)element.GetValue(ConverterProperty);
}
/// <summary>Property getter for attached property Converter.</summary>
/// <param name="element">The dependency object to which the attached property applies.</param>
/// <param name="value">The converter to associate.</param>
public static void SetConverter(DependencyObject element, IConverter value)
{
element.SetValue(ConverterProperty, value);
}
/// <summary>Property getter for attached property Converters.</summary>
/// <param name="element">The dependency object to which the attached property applies.</param>
/// <returns>Returns the collection of converters associated with the specified dependency object.</returns>
public static IList<IConverter> GetConverters(DependencyObject element)
{
return (IList<IConverter>)element.GetValue(ConverterProperty);
}
/// <summary>Property getter for attached property Converters.</summary>
/// <param name="element">The dependency object to which the attached property applies.</param>
/// <param name="value">The converters to associate.</param>
public static void SetConverters(DependencyObject element, IList<IConverter> value)
{
element.SetValue(ConverterProperty, value);
}
}
/// <summary>A strongly-typed base converter.</summary>
/// <typeparam name="Tin">The input type.</typeparam>
/// <typeparam name="Tout">The output type.</typeparam>
public abstract class BaseConverter<Tin, Tout>: DependencyObject, IConverter
{
/// <summary>The Input dependency property.</summary>
public static readonly DependencyProperty InputProperty;
/// <summary>The Output dependency property.</summary>
public static readonly DependencyProperty OutputProperty;
static BaseConverter()
{
OutputProperty = DependencyProperty.Register("Output",
typeof(Tout),
typeof(BaseConverter<Tin, Tout>),
new PropertyMetadata(GetDefault(typeof(Tout)), OutChanged));
InputProperty = DependencyProperty.Register("Input",
typeof(Tin),
typeof(BaseConverter<Tin, Tout>),
new PropertyMetadata(GetDefault(typeof(Tin)), InChanged));
}
/// <summary>Gets or sets the input value to convert from.</summary>
public Tin Input
{
get { return (Tin)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
/// <summary>Gets or sets the output value to convert to.</summary>
public Tout Output
{
get { return (Tout)GetValue(OutputProperty); }
set { SetValue(OutputProperty, value); }
}
/// <summary>Gets the from type.</summary>
public static Type From
{
get { return typeof(Tin); }
}
/// <summary>Gets the to type.</summary>
public static Type To
{
get { return typeof(Tout); }
}
#region IConverter
object IConverter.Input { get => Input; set => Input = (Tin)value; }
object IConverter.Output { get => Output; set => Output = (Tout)value; }
#endregion
#region IValueConverter
object IValueConverter.Convert(object value, Type targetType, object parameter, string language)
{
return Convert((Tin)value, parameter, language);
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
{
return ConvertBack((Tout)value, parameter, language);
}
#endregion
/// <summary>Converts an input value into an output value.</summary>
/// <param name="value">The value to convert.</param>
/// <param name="parameter">An optional parameter to pass onto the conversion process (from IValueConverter).</param>
/// <param name="language">An optional language parameter to pass onto the conversion process (from IValueConverter).</param>
/// <returns>Returns the converted value.</returns>
protected abstract Tout Convert(Tin value, object parameter, string language);
/// <summary>Converts back an output value into its original input.</summary>
/// <param name="value">The value to convert.</param>
/// <param name="parameter">An optional parameter to pass onto the conversion process (from IValueConverter).</param>
/// <param name="language">An optional language parameter to pass onto the conversion process (from IValueConverter).</param>
/// <returns>Returns the converted value from output to input.</returns>
protected abstract Tin ConvertBack(Tout value, object parameter, string language);
private static object GetDefault(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
private static void InChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as BaseConverter<Tin, Tout>;
control.Output = (Tout)((d as IValueConverter).Convert(e.NewValue, typeof(Tout), null, null));
}
private static void OutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as BaseConverter<Tin, Tout>;
//control.Input = (Tin)((d as IValueConverter).ConvertBack(e.NewValue, typeof(Tin), null, null));
}
}
/// <summary>Converts Double to and from GridLength.</summary>
public sealed class DoubleToGridLengthConverter : BaseConverter<double?, GridLength?>
{
/// <summary>The GridUnit dependency property.</summary>
public static readonly DependencyProperty GridUnitProperty;
static DoubleToGridLengthConverter()
{
GridUnitProperty = DependencyProperty.Register("GridUnit",
typeof(GridUnitType),
typeof(DoubleToGridLengthConverter),
new PropertyMetadata(GridUnitType.Auto, UnitChanged));
}
/// <summary>Gets or sets the type of grid unit to be used in the conversions.</summary>
public GridUnitType GridUnit
{
get { return (GridUnitType)GetValue(GridUnitProperty); }
set { SetValue(GridUnitProperty, value); }
}
protected override GridLength? Convert(double? value, object parameter, string language)
{
return value == null || !value.HasValue
? new GridLength()
: new GridLength(value.Value, this.GridUnit);
}
protected override double? ConvertBack(GridLength? value, object parameter, string language)
{
return value == null || !value.HasValue
? default(double?)
: value.Value.Value;
}
private static void UnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as DoubleToGridLengthConverter;
control.Output = control.Convert(control.Input, null, null);
}
}
And if you want to use my AspectRatio state trigger, here it is:
/// <summary>Aspect ratios.</summary>
public enum AspectRatio
{
/// <summary>Portrait.</summary>
Portrait,
/// <summary>Landscape.</summary>
Landscape
}
/// <summary>A state trigger based on the aspect ratio of the window.</summary>
public class WindowAspectRatioTrigger: StateTriggerBase
{
/// <summary>The target orientation.</summary>
private static readonly DependencyProperty OrientationProperty;
static WindowAspectRatioTrigger()
{
OrientationProperty = DependencyProperty.Register("Orientation",
typeof(AspectRatio),
typeof(WindowAspectRatioTrigger),
new PropertyMetadata(AspectRatio.Landscape, new PropertyChangedCallback(DesiredAspectRatioChanged)));
}
public WindowAspectRatioTrigger()
{
this.OnAspectRatioChanged(this.Orientation);
Window.Current.SizeChanged += Current_SizeChanged;
}
/// <summary>Gets or sets the desired aspect ratio for the trigger.</summary>
public AspectRatio Orientation
{
get { return (AspectRatio)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
private async void Current_SizeChanged(object sender, Windows.UI.Core.WindowSizeChangedEventArgs e)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
SetActive(IsActive(e.Size, this.Orientation));
});
}
private static void DesiredAspectRatioChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as WindowAspectRatioTrigger;
control.OnAspectRatioChanged((AspectRatio)e.NewValue);
}
private static bool IsActive(Size windowSize, AspectRatio aspectRatio)
{
var currentOrientation = windowSize.Width >= windowSize.Height
? AspectRatio.Landscape
: AspectRatio.Portrait;
return aspectRatio == currentOrientation;
}
private async void OnAspectRatioChanged(AspectRatio aspectRatio)
{
var dimensions = Window.Current.Bounds;
var size = new Size(dimensions.Width, dimensions.Height);
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
SetActive(IsActive(size, aspectRatio));
});
}
}
Related
Is there a way to pass an extra argument (alongside the default argument) to a command with InvokeCommandAction from Microsoft.Xaml.Behaviors.Wpf?
Like the following:
<behaviors:Interaction.Triggers>
<behaviors:EventTrigger EventName="MouseDown">
<behaviors:InvokeCommandAction Command="{Binding Command, Mode=OneWay}" PassEventArgsToCommand="True" />
</behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
Here the passed argument is MouseButtonEventArgs:
<behaviors:Interaction.Triggers>
<behaviors:EventTrigger EventName="MouseDown">
<behaviors:InvokeCommandAction Command="{Binding Command, Mode=OneWay}" PassEventArgsToCommand="True">
<behaviors:InvokeCommandAction.CommandParameter>
<MultiBinding Converter="{StaticResource ResourceKey=CommandConverter}">
<Binding ElementName="OtherElement" Mode="OneWay" />
</MultiBinding>
</behaviors:InvokeCommandAction.CommandParameter>
</behaviors:InvokeCommandAction>
</behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
And here I want to pass the OtherElement and MouseButtonEventArgs together. Is there a way to specify the MouseButtonEventArgs argument ?
The InvokeCommandAction supports exactly one CommandParameter, which is either the event arguments or the bound command parameter. If you try to do both, the command parameter will take precedence. Since the XAML behaviors are open source, you can see it yourself in the Invoke method of the class on Github.
In order to achieve passing both, you will have to write your own action. This would be an easy task, if you could simply create a derived type of InvokeCommandAction and override Invoke, but unfortunately it is sealed. That means, you have to copy the code for InvokeCommandAction and adapt it.
To start off, create a small class that encapsulates both the event arguments and the command parameter.
public class CompositeCommandParameter
{
public CompositeCommandParameter(EventArgs eventArgs, object parameter)
{
EventArgs = eventArgs;
Parameter = parameter;
}
public EventArgs EventArgs { get; }
public object Parameter { get; }
}
Next, copy the code from GitHub. In essence, you have to replace explicit references to the InvokeCommandAction type with your custom type, here AdvancedInvokeCommandAction and of course adapt the Invoke method so that it creates a CompositeCommandParameter instance and calls the command with it.
public sealed class AdvancedInvokeCommandAction : TriggerAction<DependencyObject>
{
private string commandName;
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(AdvancedInvokeCommandAction), null);
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(AdvancedInvokeCommandAction), null);
public static readonly DependencyProperty EventArgsConverterProperty = DependencyProperty.Register("EventArgsConverter", typeof(IValueConverter), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
public static readonly DependencyProperty EventArgsConverterParameterProperty = DependencyProperty.Register("EventArgsConverterParameter", typeof(object), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
public static readonly DependencyProperty EventArgsParameterPathProperty = DependencyProperty.Register("EventArgsParameterPath", typeof(string), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
// ...other code.
public object CommandParameter
{
get { return this.GetValue(AdvancedInvokeCommandAction.CommandParameterProperty); }
set { this.SetValue(AdvancedInvokeCommandAction.CommandParameterProperty, value); }
}
// ...other code.
protected override void Invoke(object parameter)
{
if (this.AssociatedObject != null)
{
ICommand command = this.ResolveCommand();
if (command != null)
{
object eventArgs = null;
object commandParameter = this.CommandParameter;
//if no CommandParameter has been provided, let's check the EventArgsParameterPath
if (!string.IsNullOrWhiteSpace(this.EventArgsParameterPath))
{
eventArgs = GetEventArgsPropertyPathValue(parameter);
}
//next let's see if an event args converter has been supplied
if (eventArgs == null && this.EventArgsConverter != null)
{
eventArgs = this.EventArgsConverter.Convert(parameter, typeof(object), EventArgsConverterParameter, CultureInfo.CurrentCulture);
}
//last resort, let see if they want to force the event args to be passed as a parameter
if (eventArgs == null && this.PassEventArgsToCommand)
{
eventArgs = parameter;
}
if (command.CanExecute(commandParameter))
{
var compositeCommandParameter = new CompositeCommandParameter((EventArgs) eventArgs, commandParameter);
command.Execute(compositeCommandParameter);
}
}
}
}
// ...other code.
}
In your XAML code, you can now use both. Since you will most probably use this action only with both parameters, you could further customize the action to remove the PassEventArgsToCommand parameter.
<b:Interaction.Triggers>
<b:EventTrigger EventName="MouseDown">
<local:AdvancedInvokeCommandAction Command="{Binding Command, Mode=OneWay}"
PassEventArgsToCommand="True"
CommandParameter="{Binding ElementName=OtherElement}" />
</b:EventTrigger>
</b:Interaction.Triggers>
In your view model, the command will now get an object of type CompositeCommandParameter.
Here is the complete code for the AdvancedInvokeCommandAction.
public sealed class AdvancedInvokeCommandAction : TriggerAction<DependencyObject>
{
private string commandName;
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(AdvancedInvokeCommandAction), null);
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(AdvancedInvokeCommandAction), null);
public static readonly DependencyProperty EventArgsConverterProperty = DependencyProperty.Register("EventArgsConverter", typeof(IValueConverter), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
public static readonly DependencyProperty EventArgsConverterParameterProperty = DependencyProperty.Register("EventArgsConverterParameter", typeof(object), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
public static readonly DependencyProperty EventArgsParameterPathProperty = DependencyProperty.Register("EventArgsParameterPath", typeof(string), typeof(AdvancedInvokeCommandAction), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the name of the command this action should invoke.
/// </summary>
/// <value>The name of the command this action should invoke.</value>
/// <remarks>This property will be superseded by the Command property if both are set.</remarks>
public string CommandName
{
get
{
this.ReadPreamble();
return this.commandName;
}
set
{
if (this.CommandName != value)
{
this.WritePreamble();
this.commandName = value;
this.WritePostscript();
}
}
}
/// <summary>
/// Gets or sets the command this action should invoke. This is a dependency property.
/// </summary>
/// <value>The command to execute.</value>
/// <remarks>This property will take precedence over the CommandName property if both are set.</remarks>
public ICommand Command
{
get { return (ICommand)this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets the command parameter. This is a dependency property.
/// </summary>
/// <value>The command parameter.</value>
/// <remarks>This is the value passed to ICommand.CanExecute and ICommand.Execute.</remarks>
public object CommandParameter
{
get { return this.GetValue(AdvancedInvokeCommandAction.CommandParameterProperty); }
set { this.SetValue(AdvancedInvokeCommandAction.CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the IValueConverter that is used to convert the EventArgs passed to the Command as a parameter.
/// </summary>
/// <remarks>If the <see cref="Command"/> or <see cref="EventArgsParameterPath"/> properties are set, this property is ignored.</remarks>
public IValueConverter EventArgsConverter
{
get { return (IValueConverter)GetValue(EventArgsConverterProperty); }
set { SetValue(EventArgsConverterProperty, value); }
}
/// <summary>
/// Gets or sets the parameter that is passed to the EventArgsConverter.
/// </summary>
public object EventArgsConverterParameter
{
get { return (object)GetValue(EventArgsConverterParameterProperty); }
set { SetValue(EventArgsConverterParameterProperty, value); }
}
/// <summary>
/// Gets or sets the parameter path used to extract a value from an <see cref= "EventArgs" /> property to pass to the Command as a parameter.
/// </summary>
/// <remarks>If the <see cref="Command"/> propert is set, this property is ignored.</remarks>
public string EventArgsParameterPath
{
get { return (string)GetValue(EventArgsParameterPathProperty); }
set { SetValue(EventArgsParameterPathProperty, value); }
}
/// <summary>
/// Specifies whether the EventArgs of the event that triggered this action should be passed to the Command as a parameter.
/// </summary>
/// <remarks>If the <see cref="Command"/>, <see cref="EventArgsParameterPath"/>, or <see cref="EventArgsConverter"/> properties are set, this property is ignored.</remarks>
public bool PassEventArgsToCommand { get; set; }
/// <summary>
/// Invokes the action.
/// </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)
{
if (this.AssociatedObject != null)
{
ICommand command = this.ResolveCommand();
if (command != null)
{
object eventArgs = null;
object commandParameter = this.CommandParameter;
//if no CommandParameter has been provided, let's check the EventArgsParameterPath
if (!string.IsNullOrWhiteSpace(this.EventArgsParameterPath))
{
eventArgs = GetEventArgsPropertyPathValue(parameter);
}
//next let's see if an event args converter has been supplied
if (eventArgs == null && this.EventArgsConverter != null)
{
eventArgs = this.EventArgsConverter.Convert(parameter, typeof(object), EventArgsConverterParameter, CultureInfo.CurrentCulture);
}
//last resort, let see if they want to force the event args to be passed as a parameter
if (eventArgs == null && this.PassEventArgsToCommand)
{
eventArgs = parameter;
}
if (command.CanExecute(commandParameter))
{
var compositeCommandParameter = new CompositeCommandParameter((EventArgs) eventArgs, commandParameter);
command.Execute(compositeCommandParameter);
}
}
}
}
private object GetEventArgsPropertyPathValue(object parameter)
{
object commandParameter;
object propertyValue = parameter;
string[] propertyPathParts = EventArgsParameterPath.Split('.');
foreach (string propertyPathPart in propertyPathParts)
{
PropertyInfo propInfo = propertyValue.GetType().GetProperty(propertyPathPart);
propertyValue = propInfo.GetValue(propertyValue, null);
}
commandParameter = propertyValue;
return commandParameter;
}
private ICommand ResolveCommand()
{
ICommand command = null;
if (this.Command != null)
{
command = this.Command;
}
else if (this.AssociatedObject != null)
{
// todo jekelly 06/09/08: we could potentially cache some or all of this information if needed, updating when AssociatedObject changes
Type associatedObjectType = this.AssociatedObject.GetType();
PropertyInfo[] typeProperties = associatedObjectType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propertyInfo in typeProperties)
{
if (typeof(ICommand).IsAssignableFrom(propertyInfo.PropertyType))
{
if (string.Equals(propertyInfo.Name, this.CommandName, StringComparison.Ordinal))
{
command = (ICommand)propertyInfo.GetValue(this.AssociatedObject, null);
}
}
}
}
return command;
}
}
I want to implement a IValueConverter class, which converts text from textBox to TimeSpan format string. And further if user inputs simple number, it should be converted to TimeSpan, eg. 65.8 -> 0:01:05.800
The converter works fine, it is called after each keystroke, thus if I want to write 65.8, it is converted to 0:00:06 immediately.
What is wrong?
IValueConverter
Note that Solver is static class, where the time format is checked etc., this works properly
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string time = (string)value;
TimeSpan _ts = new TimeSpan();
if (Solver.OnlySeconds(time, out _ts))
{
return _ts.ToString(Solver.__TFORMAT);
}
else return time;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (string)value;
}
XAML
<TextBox x:Name="txtTime" Width="100" Height="25" HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding RelativeSource={RelativeSource self},Path=Text, Converter={StaticResource ConvertToTime}, UpdateSourceTrigger=LostFocus}"/>
Solver
public class Solver
{
#region constants
/// <summary>
/// Output TimeSpan format
/// </summary>
public const string __TFORMAT = #"%h\:mm\:ss\.fff";
/// <summary>
/// hours and minutes separator
/// </summary>
public const string __COL = ":";
/// <summary>
/// Negative sign
/// </summary>
public const string __NEG = "-";
private const int __PREC = 1000; //determines precision to 1/1000 of second
private const long __SECTOTICK = 10000000;
#endregion
/// <summary>
/// Determines if value is convertable to seconds and creates new TimeSpan
/// </summary>
/// <param name="time">Time</param>
/// <param name="TS">out, timeSpan representing Time value</param>
/// <returns>True if succesfull, false if not</returns>
#region public Methods
public static bool OnlySeconds(string time, out TimeSpan TS)
{
double dSeconds;
if (Double.TryParse(time, out dSeconds))
{
long ticks = (long)(dSeconds * __SECTOTICK);
TS = new TimeSpan(ticks);
return true;
}
TS = new TimeSpan();
return false;
}
/// <summary>
/// Determines if value is valid TimeSpan Format
/// </summary>
/// <param name="time">Time</param>
/// <param name="TS">out TimeSpan value alwas with 0 days</param>
/// <returns>true if conversions succesfull</returns>
public static bool IsTimeValue(string time, out TimeSpan TS)
{
TimeSpan _ts = new TimeSpan();
if (OnlySeconds(time,out TS))
{
return true;
}
if (TimeSpan.TryParse(time,out _ts))
{
TS = determineTimeFromString(time);
return true;
}
return false;
}
#endregion
#region private methods
/// <summary>
/// Converts selected string to TImeSpan. String has to be in valid TimeSpan format. Erases days
/// </summary>
/// <param name="sTime">Time</param>
/// <returns>Time span</returns>
private static TimeSpan determineTimeFromString(string sTime)
{
int _iColon = Regex.Matches(sTime, __COL).Count;
string _sResult;
TimeSpan _tsDays = new TimeSpan();
if (TimeSpan.TryParse(sTime, out _tsDays))
{
if (_tsDays.Days > 0)
return new TimeSpan(0, _tsDays.Hours, _tsDays.Minutes, _tsDays.Seconds, _tsDays.Milliseconds);
}
TimeSpan _ts = new TimeSpan();
if (_iColon == 1) //minutes and seconds
{
//add 0 hours and 0 days
_sResult = addTimeToTimeSpan(sTime, "0.0:");
}
else if
(_iColon == 2) //hours minutes and seconds
{
//add 0 days
_sResult = addTimeToTimeSpan(sTime, "0.");
}
else _sResult = sTime;
if (TimeSpan.TryParse(_sResult, out _ts))
{
// in all cases remove day
return new TimeSpan(0, _ts.Hours, _ts.Minutes, _ts.Seconds, _ts.Milliseconds);
}
else return _ts;
}
/// <summary>
/// Adds time days or hours to time string
/// </summary>
/// <param name="sTime">string original time</param>
/// <param name="sTimeToAdd">string to ADD</param>
/// <returns>string</returns>
private static string addTimeToTimeSpan(string sTime, string sTimeToAdd)
{
string _sResult;
if (sTime.StartsWith(__NEG))
{
_sResult = __NEG + sTimeToAdd + sTime.Remove(0, 1);
}
else _sResult = sTimeToAdd + sTime;
return _sResult;
}
#endregion region
}
}
You should create an Attached Behavior to convert TextBox.Text on TextBox.LostFocus:
TextBox.cs
public class TextBox : DependencyObject
{
#region IsTextConversionEnabled attached property
public static readonly DependencyProperty IsTextConversionEnabledProperty = DependencyProperty.RegisterAttached(
"IsTextConversionEnabled",
typeof(bool),
typeof(TextBox),
new PropertyMetadata(false, TextBox.OnIsTextConversionEnabledChanged));
public static void SetIsTextConversionEnabled([NotNull] DependencyObject attachingElement, bool value) => attachingElement.SetValue(TextBox.IsTextConversionEnabledProperty, value);
public static bool GetIsTextConversionEnabled([NotNull] DependencyObject attachingElement) => (bool) attachingElement.GetValue(TextBox.IsTextConversionEnabledProperty);
#endregion
#region Converter attached property
public static readonly DependencyProperty ConverterProperty = DependencyProperty.RegisterAttached(
"Converter",
typeof(IValueConverter),
typeof(TextBox),
new PropertyMetadata(default(IValueConverter)));
public static void SetConverter([NotNull] DependencyObject attachingElement, IValueConverter value) => attachingElement.SetValue(TextBox.ConverterProperty, value);
public static IValueConverter GetConverter([NotNull] DependencyObject attachingElement) => (IValueConverter) attachingElement.GetValue(TextBox.ConverterProperty);
#endregion
private static void OnIsTextConversionEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is System.Windows.Controls.TextBox textBox))
{
return;
}
bool isEnabled = (bool) e.NewValue;
if (isEnabled)
{
textBox.LostFocus += TextBox.ConvertTextOnLostFocus;
}
else
{
textBox.LostFocus -= TextBox.ConvertTextOnLostFocus;
}
}
private static void ConvertTextOnLostFocus(object sender, RoutedEventArgs e)
{
var textBox = sender as System.Windows.Controls.TextBox;
textBox.Text = TextBox.GetConverter(textBox)?.Convert(
textBox.Text,
typeof(string),
string.Empty,
CultureInfo.CurrentUICulture) as string ?? textBox.Text;
}
}
Usage
<Resources>
<!-- Your IValueConverter implementation -->
<DoubleToTimeStampConverter x:Key="DoubleToTimeStampConverter" />
</Resources>
<TextBox TextBox.IsTextConversionEnabled="True"
TextBox.Converter="{StaticResource DoubleToTimeStampConverter}" />
An alternative solution would be to extend TextBox and handle the LostFocus event internally.
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 think the code is self-explanatory.
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="Deleting">
<MVVMLight:EventToCommand Command="{Binding Deleting, Mode=OneWay}"
PassEventArgsToCommand="True" />
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
I have my own custom control with a delete event and want to bind it to a command in the ViewModel.
But in the view model, I have now either
public void OnDeleting(EventArgs args)
{
var e = args as MapDeletingEventArgs;
if (e == null)
throw new ArgumentNullException("args");
Database.Delete(e.Maps);
Database.Commit();
}
or worse
public void OnDeleting(MapDeletingEventArgs args)
{
if (args == null)
throw new ArgumentNullException("args");
Database.Delete(args.Maps);
Database.Commit();
}
And I know how bad it is to have view logic in the ViewModel. I can think of no better way, does anyone advice? I use the framework MVVMLight as you can see maybe.
This can be achieved with an ICommand implementation that takes a Map instance as it's command parameter:
//WARNING: all code typed in SO window
public class DeleteMapsCommand : ICommand
{
private Database _db;
public DeleteMapsCommand(Database db)
{
_db = db;
}
public void CanExecute(object parameter)
{
//only allow delete if the parameter passed in is a valid Map
return (parameter is Map);
}
public void Execute(object parameter)
{
var map = parameter as Map;
if (map == null) return;
_db.Delete(map);
_db.Commit();
}
public event EventHandler CanExecuteChanged; //ignore this for now
}
You then create a public property in your view model to expose an instance of the command
public class ViewModel
{
public ViewModel() {
//get the Database reference from somewhere?
this.DeleteMapCommand = new DeleteMapsCommand(this.Database);
}
public ICommand DeleteMapCommand { get; private set; }
}
Finally you need to bind your action to the command property and bind the command property to the map to be deleted. You haven't really given me enough of your XAML to state how this should be done in your case, but you could do something like the below with a ListBox:
<ListBox x:Name="ListOfMaps" ItemsSource="{Binding AllTheMaps}" />
<Button Command="{Binding DeleteMapCommand}" CommandParameter="{Binding SelectedItem, ElementName=ListOfMaps}">Delete Selected Map</Button>
Update
To attach the command to the event you can use an attached property:
public static class Helper
{
public static IComparable GetDeleteMapCommand(DependencyObject obj)
{
return (IComparable)obj.GetValue(DeleteMapCommandProperty);
}
public static void SetDeleteMapCommand(DependencyObject obj, IComparable value)
{
obj.SetValue(DeleteMapCommandProperty, value);
}
public static readonly DependencyProperty DeleteMapCommandProperty =
DependencyProperty.RegisterAttached("DeleteMapCommand", typeof(IComparable), typeof(Helper), new UIPropertyMetadata(null, OnDeleteMapCommandChanged));
private static void OnDeleteMapCommandChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
//when we attach the command, grab a reference to the control
var mapControl = sender as MapControl;
if (mapControl == null) return;
//and the command
var command = GetDeleteMapCommand(sender);
if (command == null) return;
//then hook up the event handler
mapControl.Deleting += (o,e) =>
{
if (command.CanExecute(e.Maps))
command.Execute(e.Maps);
};
}
}
You then need to bind the command like this:
<MapControl local:Helper.DeleteMapCommand="{Binding DeleteMapCommand}" />
Now your view model has no reference to the view-specific types.
If you don't want to hand your EventArgs off to your viewmodel, you could try using a Behavior (this is similar to Steve Greatrex's solution, but uses the Blend SDK'a behavior instead):
Here is an example I use in one of my applications.
First, here's my custom behavior base class:
/// <summary>
/// "Better" Behavior base class which allows for safe unsubscribing. The default Behavior class does not always call <see cref="Behavior.OnDetaching"/>
/// </summary>
/// <typeparam name="T">The dependency object this behavior should be attached to</typeparam>
public abstract class ZBehaviorBase<T> : Behavior<T> where T : FrameworkElement
{
private bool _isClean = true;
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
protected sealed override void OnAttached()
{
base.OnAttached();
AssociatedObject.Unloaded += OnAssociatedObjectUnloaded;
_isClean = false;
ValidateRequiredProperties();
Initialize();
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
/// </summary>
/// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
protected sealed override void OnDetaching()
{
CleanUp();
base.OnDetaching();
}
/// <summary>
/// Validates the required properties. This method is called when the object is attached, but before
/// the <see cref="Initialize"/> is invoked.
/// </summary>
protected virtual void ValidateRequiredProperties()
{
}
/// <summary>
/// Initializes the behavior. This method is called instead of the <see cref="OnAttached"/> which is sealed
/// to protect the additional behavior.
/// </summary>
protected abstract void Initialize();
/// <summary>
/// Uninitializes the behavior. This method is called when <see cref="OnDetaching"/> is called, or when the
/// <see cref="AttachedControl"/> is unloaded.
/// <para />
/// If dependency properties are used, it is very important to use <see cref="ClearValue"/> to clear the value
/// of the dependency properties in this method.
/// </summary>
protected abstract void Uninitialize();
/// <summary>
/// Called when the <see cref="AssociatedObject"/> is unloaded.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void OnAssociatedObjectUnloaded(object sender, EventArgs e)
{
CleanUp();
}
/// <summary>
/// Actually cleans up the behavior because <see cref="OnDetaching"/> is not always called.
/// </summary>
/// <remarks>
/// This is based on the blog post: http://dotnetbyexample.blogspot.com/2011/04/safe-event-detachment-pattern-for.html.
/// </remarks>
private void CleanUp()
{
if (_isClean)
{
return;
}
_isClean = true;
if (AssociatedObject != null)
{
AssociatedObject.Unloaded -= OnAssociatedObjectUnloaded;
}
Uninitialize();
}
}
Now, my concrete implementation used to attach a command to the TextBlock's "click" event
public class TextBlockClickCommandBehavior : ZBehaviorBase<TextBlock>
{
public ICommand ClickCommand
{
get { return (ICommand)GetValue(ClickCommandProperty); }
set { SetValue(ClickCommandProperty, value); }
}
public static readonly DependencyProperty ClickCommandProperty =
DependencyProperty.Register("ClickCommand", typeof(ICommand), typeof(TextBlockClickCommandBehavior));
protected override void Initialize()
{
this.AssociatedObject.MouseLeftButtonUp += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
}
protected override void Uninitialize()
{
if (this.AssociatedObject != null)
{
this.AssociatedObject.MouseLeftButtonUp -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
}
}
void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// if you want to pass a command param to CanExecute, need to add another dependency property to bind to
if (ClickCommand != null && ClickCommand.CanExecute(null))
{
ClickCommand.Execute(null);
}
}
}
And I use it like this:
<!--Make the TextBlock for "Item" clickable to display the item search window-->
<TextBlock x:Name="itemTextBlock" Text="Item:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Column="2" FontWeight="Bold">
<e:Interaction.Behaviors>
<ZViewModels:TextBlockClickCommandBehavior ClickCommand="{Binding Path=ItemSearchCommand}"/>
</e:Interaction.Behaviors>
</TextBlock>
Now, in your case, instead of passing NULL to the command's execute method, you'd want to pass your arguments' Maps collection
how to make scroll rule always at end of the ListView automatic without focus in WPF?
The answers from #pduncan and #MattBurland both have some problems, primarily that they fail to unregister the behaviour properly.
This implementation stores the handler in another attached property so that you can toggle the behaviour on and off, perhaps through a binding.
Note that this applies to ListView. If you are using ListBox, change occurrences of ListView to ListBox.
public static class ListViewExtensions
{
public static readonly DependencyProperty AutoScrollToEndProperty = DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(ListViewExtensions), new UIPropertyMetadata(OnAutoScrollToEndChanged));
private static readonly DependencyProperty AutoScrollToEndHandlerProperty = DependencyProperty.RegisterAttached("AutoScrollToEndHandler", typeof(NotifyCollectionChangedEventHandler), typeof(ListViewExtensions));
public static bool GetAutoScrollToEnd(DependencyObject obj) => (bool)obj.GetValue(AutoScrollToEndProperty);
public static void SetAutoScrollToEnd(DependencyObject obj, bool value) => obj.SetValue(AutoScrollToEndProperty, value);
private static void OnAutoScrollToEndChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listView = s as ListView;
if (listView == null)
return;
var source = (INotifyCollectionChanged)listView.Items.SourceCollection;
if ((bool)e.NewValue)
{
NotifyCollectionChangedEventHandler scrollToEndHandler = delegate
{
if (listView.Items.Count <= 0)
return;
listView.Items.MoveCurrentToLast();
listView.ScrollIntoView(listView.Items.CurrentItem);
};
source.CollectionChanged += scrollToEndHandler;
listView.SetValue(AutoScrollToEndHandlerProperty, scrollToEndHandler);
}
else
{
var handler = (NotifyCollectionChangedEventHandler)listView.GetValue(AutoScrollToEndHandlerProperty);
source.CollectionChanged -= handler;
}
}
}
Use it like this:
<ListView local:ListViewExtensions.AutoScrollToEnd="{Binding Path=AutoScroll}">
I also made it a a static class, as you don't need to instantiate it (nor does it need to derive from DependencyObject). The cast to INotifyCollectionChanged was changed so that errors surface as cast exceptions, not NREs.
you can use this , its working for me :
this.myListView.ScrollIntoView(myListView.Items[myListView.Items.Count-1]);
if you need more advanced detail you can refere to this post
Since #pduncan's answer doesn't include the actual code, here is the code from the link they posted with only a minor modification from me:
public class ListBoxExtenders : DependencyObject
{
public static readonly DependencyProperty AutoScrollToEndProperty = DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToEndChanged));
/// <summary>
/// Returns the value of the AutoScrollToEndProperty
/// </summary>
/// <param name="obj">The dependency-object whichs value should be returned</param>
/// <returns>The value of the given property</returns>
public static bool GetAutoScrollToEnd(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToEndProperty);
}
/// <summary>
/// Sets the value of the AutoScrollToEndProperty
/// </summary>
/// <param name="obj">The dependency-object whichs value should be set</param>
/// <param name="value">The value which should be assigned to the AutoScrollToEndProperty</param>
public static void SetAutoScrollToEnd(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToEndProperty, value);
}
/// <summary>
/// This method will be called when the AutoScrollToEnd
/// property was changed
/// </summary>
/// <param name="s">The sender (the ListBox)</param>
/// <param name="e">Some additional information</param>
public static void OnAutoScrollToEndChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listBox = s as ListBox;
if (listBox != null)
{
var listBoxItems = listBox.Items;
var data = listBoxItems.SourceCollection as INotifyCollectionChanged;
var scrollToEndHandler = new System.Collections.Specialized.NotifyCollectionChangedEventHandler(
(s1, e1) =>
{
if (listBox.Items.Count > 0)
{
listBoxItems.MoveCurrentToLast();
listBox.ScrollIntoView(listBoxItems.CurrentItem);
}
});
if ((bool)e.NewValue)
data.CollectionChanged += scrollToEndHandler;
else
data.CollectionChanged -= scrollToEndHandler;
}
}
}
I modified it to use MoveCurrentToLast instead of getting the last item with listBox.Items[listBox.Items.Count - 1];. The reason for this (aside from being a little cleaner anyway) is that there is an edge case where if you have duplicate items in a list, then getting listBox.Items.Count - 1 and then scrolling to that item might not scroll you to the end of the list (it could even scroll you in the opposite direction!). It would instead scroll you to the first instance of that item.
Use an Attached Property. This article will show you how:
http://michlg.wordpress.com/2010/01/17/listbox-automatically-scroll-to-bottom/
For me, these work if each record in the list box is different. If they are the same it just scrolls to the first one that matches.