Revert the BindingProxy stuff

Trying to do this properly turns out to just be unworkable
This commit is contained in:
Antony Male 2015-10-06 11:54:33 +01:00
parent 35ecc551a3
commit 54cd9cf18f
11 changed files with 27 additions and 329 deletions

View File

@ -105,7 +105,6 @@
<Compile Include="Xaml\ActionExtension.cs" />
<Compile Include="BindableCollection.cs" />
<Compile Include="BootstrapperBase.cs" />
<Compile Include="Xaml\BindingProxy.cs" />
<Compile Include="Xaml\BoolToVisibilityConverter.cs" />
<Compile Include="Xaml\CommandAction.cs" />
<Compile Include="Conductor.cs" />
@ -135,7 +134,6 @@
<Compile Include="Xaml\View.cs" />
<Compile Include="ViewManager.cs" />
<Compile Include="WindowManager.cs" />
<Compile Include="Xaml\ViewModelBindingExtension.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="MessageBoxView.xaml">

View File

@ -320,15 +320,14 @@ namespace Stylet
/// <param name="viewModel">ViewModel to bind the View to</param>
public virtual void BindViewToModel(UIElement view, object viewModel)
{
// We used to set the View.ActionTarget here. However, we now have a system for falling back to the ViewModel
// if the ActionTarget isn't found, so we'll use that instead.
logger.Info("Setting {0}'s ActionTarget to {1}", view, viewModel);
View.SetActionTarget(view, viewModel);
var viewAsFrameworkElement = view as FrameworkElement;
if (viewAsFrameworkElement != null)
{
logger.Info("Setting {0}'s DataContext and ViewModel proxy to {1}", view, viewModel);
logger.Info("Setting {0}'s DataContext to {1}", view, viewModel);
viewAsFrameworkElement.DataContext = viewModel;
View.SetViewModel(viewAsFrameworkElement, viewModel);
}
var viewModelAsViewAware = viewModel as IViewAware;

View File

@ -4,8 +4,6 @@ using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Data;
using System.Globalization;
using System.Diagnostics;
namespace Stylet.Xaml
{
@ -71,17 +69,13 @@ namespace Stylet.Xaml
this.ActionNonExistentBehaviour = actionNonExistentBehaviour;
this.logger = logger;
var multiBinding = new MultiBinding();
multiBinding.Converter = new ActionTargetMultiValueConverter();
multiBinding.Bindings.Add(new Binding()
var binding = new Binding()
{
Path = new PropertyPath(View.ActionTargetProperty),
Mode = BindingMode.OneWay,
Source = this.Subject,
});
multiBinding.Bindings.Add(View.GetBindingToViewModel(this.Subject));
BindingOperations.SetBinding(this, targetProperty, multiBinding);
};
BindingOperations.SetBinding(this, targetProperty, binding);
}
private void UpdateActionTarget(object oldTarget, object newTarget)
@ -184,27 +178,5 @@ namespace Stylet.Xaml
ExceptionDispatchInfo.Capture(e.InnerException).Throw();
}
}
private class ActionTargetMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// We expect 2 values: [0] is the actiontarget, and [1] is the viewmodel from resource
Debug.Assert(values.Length == 2);
if (values[0] != View.InitialActionTarget)
return values[0];
if (values[1] != null)
return values[1];
return View.InitialActionTarget;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
}
}

View File

@ -103,21 +103,20 @@ namespace Stylet.Xaml
// Seems this is the case when we're in a template. We'll get called again properly in a second.
// http://social.msdn.microsoft.com/Forums/vstudio/en-US/a9ead3d5-a4e4-4f9c-b507-b7a7d530c6a9/gaining-access-to-target-object-instead-of-shareddp-in-custom-markupextensions-providevalue-method?forum=wpf
var targetObjectAsDependencyObject = valueService.TargetObject as DependencyObject;
if (targetObjectAsDependencyObject == null)
if (!(valueService.TargetObject is DependencyObject))
return this;
var propertyAsDependencyProperty = valueService.TargetProperty as DependencyProperty;
if (propertyAsDependencyProperty != null && propertyAsDependencyProperty.PropertyType == typeof(ICommand))
{
// If they're in design mode and haven't set View.ActionTarget, default to looking sensible
return new CommandAction(targetObjectAsDependencyObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour);
return new CommandAction((DependencyObject)valueService.TargetObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour);
}
var propertyAsEventInfo = valueService.TargetProperty as EventInfo;
if (propertyAsEventInfo != null)
{
var ec = new EventAction(targetObjectAsDependencyObject, propertyAsEventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
var ec = new EventAction((DependencyObject)valueService.TargetObject, propertyAsEventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
return ec.GetDelegate();
}
@ -128,7 +127,7 @@ namespace Stylet.Xaml
var parameters = propertyAsMethodInfo.GetParameters();
if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType))
{
var ec = new EventAction(targetObjectAsDependencyObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
var ec = new EventAction((DependencyObject)valueService.TargetObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour);
return ec.GetDelegate();
}
}

View File

@ -1,46 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Stylet.Xaml
{
internal class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new PropertyMetadata(null));
}
/// <summary>
/// Converter which extracts the 'Data' property from a BindingProxy.
/// </summary>
internal class BindingProxyToValueConverter : IValueConverter
{
public static readonly BindingProxyToValueConverter Instance = new BindingProxyToValueConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var proxy = value as BindingProxy;
if (proxy != null)
return proxy.Data;
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
}

View File

@ -3,7 +3,6 @@ using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Reflection;
using Stylet.Logging;
namespace Stylet.Xaml
{
@ -12,11 +11,10 @@ namespace Stylet.Xaml
/// </summary>
public static class View
{
private static readonly ILogger logger = LogManager.GetLogger(typeof(View));
internal const string ViewManagerResourceKey = "b9a38199-8cb3-4103-8526-c6cfcd089df7";
internal const string ViewModelProxyResourceKey = "8b7cb732-8a14-4813-a580-b1f3cccea7b7";
/// <summary>
/// Key which will be used to retrieve the ViewManager associated with the current application, from application's resources
/// </summary>
public const string ViewManagerResourceKey = "b9a38199-8cb3-4103-8526-c6cfcd089df7";
/// <summary>
/// Initial value of the ActionTarget property.
@ -48,7 +46,7 @@ namespace Stylet.Xaml
/// The object's ActionTarget. This is used to determine what object to call Actions on by the ActionExtension markup extension.
/// </summary>
public static readonly DependencyProperty ActionTargetProperty =
DependencyProperty.RegisterAttached("ActionTarget", typeof(object), typeof(View), new FrameworkPropertyMetadata(InitialActionTarget));
DependencyProperty.RegisterAttached("ActionTarget", typeof(object), typeof(View), new FrameworkPropertyMetadata(InitialActionTarget, FrameworkPropertyMetadataOptions.Inherits));
/// <summary>
/// Fetch the ViewModel currently associated with a given object
@ -107,81 +105,6 @@ namespace Stylet.Xaml
}
}));
// Dependency Properties with 'inherit' set are great, except when they're not. In particular, they lose their
// value when you cross a boundary into an element which does not sit inside the visual (or logica?) tree.
// For example, trying to get the value of a previously-set View.ActionTarget from a KeyBinding will fail, because
// the KeyBinding does not sit inside either the visual or logical tree, and so View.ActionTarget loses its value.
// However, there are ways around this. If you create an instance of an object inheriting from Freezable and set it
// as a resource on some parent, you can later retrieve that resource even from children where inherited Dependency
// Properties will fail, using {DynamicResource}.
// Therefore. We have a class called BindingProxy, which has a single object 'Data' property and which inherits from
// freezable. View.SetViewModel sets this as a resource (with key ViewModelProxyResourceKey) on whatever FrameworkElement
// it's given, and sets the ViewModel as the 'Data' property. Later on, we can retrieve that BindingProxy from its
// key, and extract the ViewModel from it.
// Normally we'd be able to use FrameworkElement.SetResourceReference to emulate {DynamicResource} from code, but
// irritatingly that's defined on FrameworkElement and not DependencyObject (even though it does nothing specific to
// FrameworkElement), so won't work with e.g. KeyBinding (which inherits from DependencyObject but not FrameworkElement).
// However, DynamicResourceExtension.ProvideValue doesn't actually require a service provider, so we can get away with
// using it directly (it just wraps ResourceReferenceExpression, but that's internal, boo).
// Because the result of {DynamicResource} can change, we need to assign it to a Dependency Property (we use
// View.ViewModelProxy), and we return a binding which binds to that Dependency Property, and also has a converter
// which extracts the 'Data' property from the BindingProxy.
// The final step in the puzzle is that ActionBase will first look for a View.ActionTarget, and if not set, it will
// then look for a ViewModel using View.GetBindingToViewModel.
// The ViewModelExtension Markup Extension also uses this mechanism.
// To recap: if someone wants to get the ViewModel associated with a particular UI element, they can call
// View.GetBindingToViewModel. This will create a lookup to our previously-stored BindingProxy resource (whose 'Data'
// property is set to the ViewModel) using DynamicResourceExtension, and attach that to the View.ViewModelProxy
// Dependency Property. It will then return a binding, which can be used to fetch the ViewModel. This mechanism
// is used by {s:ViewModel}, and by ActionBase when an ActionTarget is otherwise not available.
/// <summary>
/// Set the ViewModel which can be subsequently retrieved using {s:ViewModel}
/// </summary>
/// <param name="view">View to store the ViewModel for</param>
/// <param name="viewModel">ViewModel to store</param>
public static void SetViewModel(FrameworkElement view, object viewModel)
{
var bindingProxy = new BindingProxy()
{
Data = viewModel,
};
view.Resources[ViewModelProxyResourceKey] = bindingProxy;
}
internal static void EnsureViewModelProxyValueSetUp(DependencyObject view)
{
if (view.GetValue(ViewModelProxyProperty) == null)
{
var resource = new DynamicResourceExtension(ViewModelProxyResourceKey).ProvideValue(null);
view.SetValue(ViewModelProxyProperty, resource);
}
}
/// <summary>
/// Fetch a binding which can be used to retrieve the ViewModel associated with a View
/// </summary>
/// <param name="view">View to fetch the ViewModel for</param>
/// <returns>Binding which can retrieve the ViewModel</returns>
public static Binding GetBindingToViewModel(DependencyObject view)
{
EnsureViewModelProxyValueSetUp(view);
var binding = new Binding()
{
Source = view,
Path = new PropertyPath(ViewModelProxyProperty),
Mode = BindingMode.OneWay,
Converter = BindingProxyToValueConverter.Instance,
};
return binding;
}
internal static readonly DependencyProperty ViewModelProxyProperty =
DependencyProperty.RegisterAttached("ViewModelProxy", typeof(BindingProxy), typeof(View), new PropertyMetadata(null));
/// <summary>
/// Helper to set the Content property of a given object to a particular View
/// </summary>

View File

@ -1,96 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace Stylet.Xaml
{
/// <summary>
/// MarkupExtension which can retrieve the ViewModel for the current View, if available
/// </summary>
public class ViewModelBindingExtension : MarkupExtension
{
/// <summary>
/// Gets or sets the path to the property
/// </summary>
public string Path { get; set; }
/// <summary>
/// Gets or sets the converter to use.
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
public CultureInfo ConverterCulture { get; set; }
/// <summary>
/// Gets or sets the parameter to pass to the Converter.
/// </summary>
public object ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to return a value
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets a string that specifies how to format the binding if it displays the bound value as a string
/// </summary>
public string StringFormat { get; set; }
/// <summary>
/// Gets or sets the value that is used in the target when the value of the source is null
/// </summary>
public object TargetNullValue { get; set; }
/// <summary>
/// Instantiates a new instance of the <see cref="ViewModelBindingExtension"/> class
/// </summary>
public ViewModelBindingExtension()
{
}
/// <summary>
/// Initializes a new instance of the Binding class with an initial path
/// </summary>
/// <param name="path">The initial Path for the binding</param>
public ViewModelBindingExtension(string path)
{
this.Path = path;
}
/// <summary>
/// When implemented in a derived class, returns an object that is provided as the
/// value of the target property for this markup extension.
/// </summary>
/// <param name="serviceProvider">A service provider helper that can provide services for the markup extension.</param>
/// <returns>The object value to set on the property where the extension is applied.</returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
var valueService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
var targetObjectAsDependencyObject = valueService.TargetObject as DependencyObject;
if (targetObjectAsDependencyObject == null)
return this;
View.EnsureViewModelProxyValueSetUp(targetObjectAsDependencyObject);
var binding = new Binding()
{
Source = targetObjectAsDependencyObject,
Path = new PropertyPath("(0).(1)." + this.Path, View.ViewModelProxyProperty, BindingProxy.DataProperty),
Mode = BindingMode.OneWay,
Converter = this.Converter,
ConverterCulture = this.ConverterCulture,
ConverterParameter = this.ConverterParameter,
FallbackValue = this.FallbackValue,
StringFormat = this.StringFormat,
TargetNullValue = this.TargetNullValue,
};
return binding.ProvideValue(serviceProvider);
}
}
}

View File

@ -2,16 +2,16 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
Title="ShellView" Height="500" Width="500" SizeToContent="Height">
Title="ShellView" Height="500" Width="500">
<DockPanel LastChildFill="False">
<GroupBox DockPanel.Dock="Top" Header="ShowDialog and DialogResult" Padding="10">
<StackPanel Orientation="Horizontal">
<Button Command="{s:Action ShowDialogAndDialogResult}" Content="Show Dialog"/>
<TextBlock Margin="50,0,0,0">Result: </TextBlock>
<Button Command="{s:Action ShowDialogAndDialogResult}">Show Dialog</Button>
<TextBlock Margin="50,0,0,0">Result:</TextBlock>
<TextBlock Margin="10,0,0,0" Text="{Binding ShowDialogAndDialogResultDialogResult}"/>
</StackPanel>
</GroupBox>
<GroupBox DockPanel.Dock="Top" Header="Window Lifecycle" Padding="10">
<Button Command="{s:Action ShowWindowLifecycle}">Show Window</Button>
</GroupBox>
@ -29,11 +29,10 @@
<Button DockPanel.Dock="Top" Command="{s:Action TestDispatcher}">Test Dispatcher</Button>
</DockPanel>
</GroupBox>
<GroupBox DockPanel.Dock="Top" Header="ActionTarget" Padding="10">
<DockPanel>
<TextBlock DockPanel.Dock="Top" TextWrapping="WrapWithOverflow">
Verify that pressing ctrl+s in the text box creates a dialog. Also verify that right-clicking in the text box and clicking the menu item shows a dialog.
Verify that pressing ctrl+s in the text box creates a dialog. Also verify that right-clicking in the text box and clicking the menu item shows a dialog.
</TextBlock>
<TextBox DockPanel.Dock="Top">
<TextBox.InputBindings>
@ -45,14 +44,6 @@
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
<Button DockPanel.Dock="Top" s:View.ActionTarget="{Binding ChildViewModel}" Command="{s:Action Foo}">Click Me</Button>
</DockPanel>
</GroupBox>
<GroupBox DockPanel.Dock="Top" Header="{}{ViewModel}" Padding="10">
<DockPanel DataContext="{x:Null}">
<TextBlock DockPanel.Dock="Top">Ensure that the label below displays the text "Pass":</TextBlock>
<TextBlock DockPanel.Dock="Top" Text="{s:ViewModelBinding ViewModelTestLabel}"/>
</DockPanel>
</GroupBox>
</DockPanel>

View File

@ -68,16 +68,6 @@ namespace StyletIntegrationTests
else
this.windowManager.ShowMessageBox("Failure");
}
public void ShowActionTargetSaved()
{
this.windowManager.ShowMessageBox("PASS!");
}
public string ViewModelTestLabel
{
get { return "Pass"; }
}
}
public class ChildViewModel

View File

@ -304,13 +304,15 @@ namespace StyletUnitTests
}
[Test]
public void BindViewToModelDoesNotSetActionTarget()
public void BindViewToModelSetsActionTarget()
{
var view = new UIElement();
var model = new object();
var viewManager = new AccessibleViewManager(type => null, new List<Assembly>());
viewManager.BindViewToModel(view, new object());
Assert.AreEqual(View.InitialActionTarget, View.GetActionTarget(view));
viewManager.BindViewToModel(view, model);
Assert.AreEqual(model, View.GetActionTarget(view));
}
[Test]

View File

@ -5,9 +5,7 @@ using Stylet.Xaml;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
namespace StyletUnitTests
{
@ -27,19 +25,6 @@ namespace StyletUnitTests
}
}
private class TestObjectWithDP : DependencyObject
{
public object DP
{
get { return (object)GetValue(DPProperty); }
set { SetValue(DPProperty, value); }
}
public static readonly DependencyProperty DPProperty =
DependencyProperty.Register("DP", typeof(object), typeof(TestObjectWithDP), new PropertyMetadata(null));
}
private Mock<IViewManager> viewManager;
[SetUp]
@ -66,7 +51,7 @@ namespace StyletUnitTests
public void ModelStores()
{
var obj = new FrameworkElement();
obj.Resources.Add("b9a38199-8cb3-4103-8526-c6cfcd089df7", this.viewManager.Object);
obj.Resources.Add(View.ViewManagerResourceKey, this.viewManager.Object);
View.SetModel(obj, 5);
Assert.AreEqual(5, View.GetModel(obj));
}
@ -75,7 +60,7 @@ namespace StyletUnitTests
public void ChangingModelCallsOnModelChanged()
{
var obj = new FrameworkElement();
obj.Resources.Add("b9a38199-8cb3-4103-8526-c6cfcd089df7", this.viewManager.Object);
obj.Resources.Add(View.ViewManagerResourceKey, this.viewManager.Object);
var model = new object();
View.SetModel(obj, null);
@ -170,24 +155,5 @@ namespace StyletUnitTests
var content = (TextBlock)element.Content;
Assert.AreEqual("View for TestViewModel.SubViewModel", content.Text);
}
[Test]
public void ViewModelCanBeRetrievedByChildren()
{
var view = new UserControl();
var viewModel = new object();
View.SetViewModel(view, viewModel);
// Use something that doesn't inherit attached properties
var keyBinding = new KeyBinding();
view.InputBindings.Add(keyBinding);
var binding = View.GetBindingToViewModel(keyBinding);
var receiver = new TestObjectWithDP();
BindingOperations.SetBinding(receiver, TestObjectWithDP.DPProperty, binding);
Assert.AreEqual(viewModel, receiver.DP);
}
}
}