I used to assign directly to the set of points, the graphics can be displayed properly, and later found that the performance of doing that is very poor, I thought of using the binding method to do it.
After WPF Polygon binding, the set of points has changed, but the graphics have not changed.
xaml code
<Canvas Background="Black" Name="map">
<Polygon Name="pl" Points="{Binding sendPoints,Mode=TwoWay}"></Polygon>
</Canvas>
backend code
DrawLinesClass drawLinesClass = new DrawLinesClass();
pl.DataContext = drawLinesClass;//bind
pl.Stroke = Brushes.Red;
pl.StrokeThickness = 2;
Thread td = new Thread(() =>
{
double index = 0,sum=0;
while (true)
{
Thread.Sleep(50);
if (isRun)
{
sum+=0.01;
pl.Dispatcher.Invoke(new Action(() =>
{
if (sum * 2 >= map.ActualHeight - 40)
{
sum = 0;
index += 1;
//drawLinesClass.sendPoints.Clear();
}
drawLinesClass.sendPoints.Add(new Point(index * sum, sum * 2));
//pl.Points = drawLinesClass.sendPoints;//no bind
}));
}
}
});
td.IsBackground = true;
td.Start();
bind model
public class DrawLinesClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private PointCollection _sendPointsList;
public PointCollection sendPoints
{
get
{
if (_sendPointsList == null) _sendPointsList = new PointCollection();
return _sendPointsList;
}
set
{
//this._sendPointsList = new PointCollection();
this._sendPointsList = value;
OnPropertyChanged("sendPoints");
}
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now the set of binding points has changed, I hope the graphics can change automatically.
The Binding
Points="{Binding sendPoints}"
where (Mode=TwoWay was pointless) only updates its target property Points if you assign a new value to its source property sendPoints. Just adding points to the existing sendPoints collection as in
drawLinesClass.sendPoints.Add(new Point(index * sum, sum * 2));
won't make the Binding update.
In other scenarios like binding the ItemsSource property of an ItemsControl you would simply use an ObservableCollection for the source property. However, this won't work here. You really have to create a new PointCollection whenever you add a point.
Besides that, better use a DispatcherTimer instead of a Thread with Sleep and Dispatcher.Invoke:
double index = 0,sum=0;
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50)
};
timer.Tick += (s, e) =>
{
sum += 0.01;
if (sum * 2 >= map.ActualHeight - 40)
{
sum = 0;
index += 1;
}
var points = new PointCollection(drawLinesClass.sendPoints);
points.Add(new Point(index * sum, sum * 2));
drawLinesClass.sendPoints = points;
};
timer.Start();
To stop the the timer, just call
timer.Stop();
Also better write
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
As a final remark, the Bindings in your view should usually use a common view model, and hence a common DataContext that is assigned at the MainWindow instance. When you explicitly set a Polyline's DataContext in code behind like
pl.DataContext = drawLinesClass;
it is questionably why you would at all bind the Polylines's Points property. Instead of Binding and setting the DataContext, you could as well simply write
pl.Points = drawLinesClass.sendPoints;
and thus even avoid the need for implementing INotifyPropertyChanged. You would also avoid the need for creating a new PointsCollection on each update, because
drawLinesClass.sendPoints.Add(new Point(index * sum, sum * 2))
will now magically work.
Related
I have been reading Mark Seeman's book on dependency injection in .NET and I'm struggling to configure composition root in WPF application.
My container will be registered in the application startup method:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var container = new Container();
container.Configure(r =>
{
r.For<IAccountServices>().Use<AccountServicesProxy>();
r.For<MainWindow>().Use<MainWindow>();
});
}
This makes sense as the application startup represents my composition root.
WPF windows in my application are based on view models. View models use constructor injection. E.g. I may compose a view model by injecting implementation of IAccountServices.
When it comes to creating my main window, I can do the following inside of the OnStartup method:
var mainWindow = container.GetInstance<MainWindow>();
mainWindow.Show();
Once I'm inside of the main window, I might want open up another window. So far I've been able to come up with one way of doing this, which is to create a window factory and ask window factory to resolve instance of the window. I'll have to make sure that window factory is available in every view model that might need to open a new window. In my mind this is as bad as passing IoC container around my application (service locator anti-pattern comes to mind).
Does this approach seem right to you? My gut feeling tells me that this is wrong, but I haven't come up with a better way of achieving this (yet).
I think before implement patterns of behavior, such as a Mediator, and the like, need to decide on a generic pattern for easy application structure. For this purpose, namely, for the create independent windows, well suited Abstract factory pattern.
Creation of the windows can be implemented on the side ViewModel using methods such as IDialogService. But I think that this task should be implemented on the side View, because the Window object refers to the View and not to ViewModel. So, you must create MVVM style architecture that it allows create independent windows using design patterns.
I created a project in which an Abstract factory creates a Window on the side of the View using the attached behavior. Abstract factory also implements the Singleton pattern to create a global point of access and to ensure the uniqueness of the newly constructed object. Attached behavior implicitly implements pattern Decorator who is a wrapper for an abstract factory that is used on the side of XAML. To an Abstract factory does not refer to objects which are located in ViewModel is used a Proxy pattern which is a ContentControl with DataTemplate without DataType. Also used Command pattern for independent action between objects. As a result, this project uses the following patterns:
Abstract factory
Singleton
Decorator
Proxy
Command
The project structure looks like this:
In the attached behavior has attached dependency property Name, which is transmitted in the name of the new window. For him registered PropertyChangedEvent, which is a call Make method an abstract factory:
private static void IsFactoryStart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var window = sender as Window;
if (window == null)
{
return;
}
if (e.NewValue is String && String.IsNullOrEmpty((string)e.NewValue) == false)
{
_typeWindow = (string)e.NewValue;
if (_typeWindow != null)
{
var newWindow = WindowFactory.Instance.Make(_typeWindow);
newWindow.Show();
}
}
}
WindowFactory together with the Singleton pattern looks like this:
public class WindowFactory : IWindowFactory
{
#region WindowFactory Singleton Instance
private static WindowFactory _instance = null;
private static readonly object padlock = new object();
public static WindowFactory Instance
{
get
{
lock (padlock)
{
if (_instance == null)
{
_instance = new WindowFactory();
}
return _instance;
}
}
}
#endregion
public Window Make(string TypeWindow)
{
if (TypeWindow.Equals("WindowOneViewProxy"))
{
var windowOne = new Window();
windowOne.Width = 450;
windowOne.Height = 250;
windowOne.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowOne.Title = TypeWindow;
windowOne.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowOne;
}
else if (TypeWindow.Equals("WindowTwoViewProxy"))
{
var windowTwo = new Window();
windowTwo.Width = 500;
windowTwo.Height = 200;
windowTwo.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowTwo.Title = TypeWindow;
windowTwo.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowTwo;
}
else if (TypeWindow.Equals("WindowThreeViewProxy"))
{
var windowThree = new Window();
windowThree.Width = 400;
windowThree.Height = 140;
windowThree.WindowStartupLocation = WindowStartupLocation.CenterScreen;
windowThree.Title = TypeWindow;
windowThree.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;
return windowThree;
}
else
throw new Exception("Factory can not create a: {0}" + TypeWindow);
}
}
For the property Window.ContentTemplate set DataTemplate from resources. ContentTemplate is responsible for the visual representation, in order to bind properties from ViewModel, you need to set the object to Content. But in this case, the Abstract factory reference will to ViewModel, and to avoid them and using the proxy pattern as follows:
WindowOneProxyView
<DataTemplate x:Key="WindowOneViewProxy">
<ContentControl ContentTemplate="{StaticResource WindowOneViewRealObject}">
<ViewModels:WindowOneViewModel />
</ContentControl>
</DataTemplate>
WindowOneViewRealObject
<DataTemplate x:Key="WindowOneViewRealObject" DataType="{x:Type ViewModels:WindowOneViewModel}">
<Grid>
<Label Content="{Binding Path=WindowOneModel.TextContent}"
HorizontalAlignment="Center"
VerticalAlignment="Top"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="Beige" />
<Button Content="One command"
Width="100"
Height="30"
HorizontalAlignment="Center"
Command="{Binding OneCommand}" />
</Grid>
</DataTemplate>
In DataTemplate proxy is not specified DataType, but it is in the real object.
In MainViewModel has commands to simply set the window name, which will give input for attached behavior:
MainModel
public class MainModel : NotificationObject
{
#region TypeName
private string _typeName = null;
public string TypeName
{
get
{
return _typeName;
}
set
{
_typeName = value;
NotifyPropertyChanged("TypeName");
}
}
#endregion
}
MainViewModel
public class MainViewModel
{
#region MainModel
private MainModel _mainModel = null;
public MainModel MainModel
{
get
{
return _mainModel;
}
set
{
_mainModel = value;
}
}
#endregion
#region ShowWindowOneCommand
private ICommand _showWindowOneCommand = null;
public ICommand ShowWindowOneCommand
{
get
{
if (_showWindowOneCommand == null)
{
_showWindowOneCommand = new RelayCommand(param => this.ShowWindowOne(), null);
}
return _showWindowOneCommand;
}
}
private void ShowWindowOne()
{
MainModel.TypeName = "WindowOneViewProxy";
}
#endregion
#region ShowWindowTwoCommand
private ICommand _showWindowTwoCommand = null;
public ICommand ShowWindowTwoCommand
{
get
{
if (_showWindowTwoCommand == null)
{
_showWindowTwoCommand = new RelayCommand(param => this.ShowWindowTwo(), null);
}
return _showWindowTwoCommand;
}
}
private void ShowWindowTwo()
{
MainModel.TypeName = "WindowTwoViewProxy";
}
#endregion
#region ShowWindowThreeCommand
private ICommand _showWindowThreeCommand = null;
public ICommand ShowWindowThreeCommand
{
get
{
if (_showWindowThreeCommand == null)
{
_showWindowThreeCommand = new RelayCommand(param => this.ShowWindowThree(), null);
}
return _showWindowThreeCommand;
}
}
private void ShowWindowThree()
{
MainModel.TypeName = "WindowThreeViewProxy";
}
#endregion
public MainViewModel()
{
MainModel = new MainModel();
}
}
MainWindow looks as:
<Window x:Class="WindowFactoryNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WindowFactoryNamespace.ViewModels"
xmlns:AttachedBehaviors="clr-namespace:WindowFactoryNamespace.AttachedBehaviors"
AttachedBehaviors:WindowFactoryBehavior.Name="{Binding Path=MainModel.TypeName}"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="300" Width="300">
<Window.DataContext>
<this:MainViewModel />
</Window.DataContext>
<WrapPanel>
<Button Content="WindowOne"
Margin="10"
Command="{Binding ShowWindowOneCommand}" />
<Button Content="WindowTwo"
Margin="10"
Command="{Binding ShowWindowTwoCommand}" />
<Button Content="WindowThree"
Margin="10"
Command="{Binding ShowWindowThreeCommand}" />
</WrapPanel>
</Window>
Test View-ViewModel for the first window looks like this (they practically identical):
WindowOneModel
public class WindowOneModel : NotificationObject
{
#region TextContent
private string _textContent = "Text content for WindowOneView";
public string TextContent
{
get
{
return _textContent;
}
set
{
_textContent = value;
NotifyPropertyChanged("TextContent");
}
}
#endregion
}
WindowOneViewModel
public class WindowOneViewModel
{
#region WindowOneModel
private WindowOneModel _windowOneModel = null;
public WindowOneModel WindowOneModel
{
get
{
return _windowOneModel;
}
set
{
_windowOneModel = value;
}
}
#endregion
#region OneCommand
private ICommand _oneCommand = null;
public ICommand OneCommand
{
get
{
if (_oneCommand == null)
{
_oneCommand = new RelayCommand(param => this.One(), null);
}
return _oneCommand;
}
}
private void One()
{
WindowOneModel.TextContent = "Command One change TextContent";
}
#endregion
public WindowOneViewModel()
{
WindowOneModel = new WindowOneModel();
}
}
This project is available at this link.
Output
MainWindow
WindowOne
WindowTwo
WindowThree
IMHO, there is no need to over complicate the solution for the sake of MVVM purity. You risk the subsequent developers not understanding your elegant solution and break it. In fact there is a good chance of that as "pure" implementations tend to be not that readable because of the complexity.
IMHO, any solution where a problem is permanently solved under an abstraction with minimal code overhead and simplicity in its usage is better than doing considerable overhead every time the solution is used even if "purity" is achieved(it won't serve any purpose). The problem of showing dialog in the application has to be solved once and it should be easy to use it in the future.
Composing view models is perfectly fine, and could make life easier by allowing view models to interact without drama
A dialog service can be created which will act as a wrapper for all your dialog needs in the application. You can inject the Dialog Service and the child view models which needs to be displayed in a window, to your parent view model. When you need to display the window, ask the Dialog service to do it, passing it the view model instance and view name.
Note:code is not complied or tested
public class DialogService : IDialogService
{
IEventAggregator _eventAggregator;
bool _fatalError;
//Provides a wrapper function which will connect your view and view model and open a
//dialog
public Window ShowCustomDialog<TViewModel>(string name, TViewModel viewModel, bool
modal, double left, double top, Action<bool?> OnClose, int width, int height)
{
if (_fatalError == true)
{
return null;
}
Window view = new Window(name);
if (viewModel != null)
{
view.DataContext = viewModel;
}
if (left != -1.0 && top != -1.0)
{
view.WindowStartupLocation = WindowStartupLocation.Manual;
view.Left = left;
view.Top = top;
}
else
{
view.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
if (width != -1 && height != -1)
{
view.Width = width;
view.Height = height;
}
view.Closed += (o, e) =>
{
_eventAggregator.GetEvent<NotifyDialogAction>().Publish(false);
if (OnClose != null)
{
OnClose(e.DialogResult);
}
};
view.Loaded += (o, e) =>
{
_eventAggregator.GetEvent<NotifyDialogAction>().Publish(true);
Window window = o as Window;
if (window != null)
{
double dialogWidth = window.ActualWidth;
double screenWidth =
Application.Current.RootVisual.RenderSize.Width;
double dialogLeft = window.Left;
if (dialogLeft + dialogWidth > screenWidth)
{
window.Left = screenWidth - dialogWidth;
}
double dialogHeight = window.ActualHeight;
double screenHeight =
Application.Current.RootVisual.RenderSize.Height;
double dialogTop = window.Top;
if (dialogTop + dialogHeight > screenHeight)
{
window.Top = screenHeight - dialogHeight;
}
}
};
if (modal)
{
view.ShowDialog();
}
else
{
view.Show();
}
return view;
}
//Add more functions. For example to pop up a message box etc.
}
Usage
public class ComposedVM
{
public ViewModelA objA{get;set;}
public ViewModelB objB{get;set;}
IDialogService dialogService{get;set;}
public ComposedVM(ViewModelA a, ViewModelB b, IDialogService dlg )
{
objA = a;
objB = b;
dialogService = dlg
}
public void OnShowWindowACommand()
{
dialogService .ShowCustomDialog<object>(
DialogNames.ViewA/*view name constant*/, objA, true, -1.0, -1.0,
result =>
{
if (result == true)
{
dialogService.ShowMessageDialog(ApplicationStrings.SuccessFulOperation);
}
});
}
}
An event/message based communication can be used between modules. Using it for related view models in a module is an overkill IMHO.
Pushing container instance through constructor is a bad idea in 99% of cases, because container is a service locator. The main disadvantages of this approach are:
dependency from concrete implementation of container;
unclear API of your classes, which also leads to fragile unit tests.
There are many ways to create window in MVVM fashion:
using Mediators (like IMessenger in MvvmLight, IEventAggregator in Caliburn.Micro);
using special IDialogService;
using attached behaviours;
using Action that inserted via ViewModel constructor;
using Controllers.
I have a very simple WPF application that renders simple shapes in a canvas:
The blue squares are ItemsControl and the red circles are Controls
The following step in my application is adding connection lines between the shapes. The shaphes will be moved and I want the connections to be automatically moved. I readed about how to do it adding connection bindings.
All worked fine with canvas direct children (container), but if I want to connect the nodes, it does not work. It seems that if I don't call Canvas.SetLeft() and Canvas.SetTop() explicitily, then Canvas.GetLeft() and Canvas.GetTop() return NAN.
How should I proceed?
Should I implement a mechanism to get all objects placed in my canvas, so I always can calculate Canvas.GetLeft() over all of them?
Should I proceed in another way?
Source code and screenshot
This is the source code of the example. You can find here the complete example:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Container container1 = new Container() { Width = 100, Height = 100 };
Node node1 = new Node() { Width = 50, Height = 50 };
container1.Items.Add(node1);
Container container2 = new Container() { Width = 100, Height = 100 };
Node node2 = new Node() { Width = 50, Height = 50 };
container2.Items.Add(node2);
Canvas.SetLeft(container2, 200);
myCanvas.Children.Add(container1);
myCanvas.Children.Add(container2);
}
}
class Container : ItemsControl
{
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(
Brushes.Blue, null, new Rect(0, 0, this.Width, this.Height));
}
}
class Node : Control
{
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawEllipse(
Brushes.Red, null,
new Point(Width / 2, Height / 2), Width / 2, Height / 2);
}
}
This is how I implemented the connections between the shapes:
public Shape AddConnection(UIElement source, UIElement target)
{
Connector conn = new Connector();
conn.SetBinding(Connector.StartPointProperty,
CreateConnectorBinding(source));
conn.SetBinding(Connector.EndPointProperty,
CreateConnectorBinding(target));
return conn;
}
private MultiBinding CreateConnectorBinding(UIElement connectable)
{
// Create a multibinding collection and assign an appropriate converter to it
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = new ConnectorBindingConverter();
// Create binging #1 to IConnectable to handle Left
Binding binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(Canvas.LeftProperty);
multiBinding.Bindings.Add(binding);
// Create binging #2 to IConnectable to handle Top
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(Canvas.TopProperty);
multiBinding.Bindings.Add(binding);
// Create binging #3 to IConnectable to handle ActualWidth
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(FrameworkElement.ActualWidthProperty);
multiBinding.Bindings.Add(binding);
// Create binging #4 to IConnectable to handle ActualHeight
binding = new Binding();
binding.Source = connectable;
binding.Path = new PropertyPath(FrameworkElement.ActualHeightProperty);
multiBinding.Bindings.Add(binding);
return multiBinding;
}
The Connector object is very simple. It has a LineGeometry and exposes two DependencyProperties to calculate the start point and the end point.
public static readonly DependencyProperty StartPointProperty =
DependencyProperty.Register(
"StartPoint",
typeof(Point),
typeof(Connector),
new FrameworkPropertyMetadata(
new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty EndPointProperty =
DependencyProperty.Register(
"EndPoint",
typeof(Point),
typeof(Connector),
new FrameworkPropertyMetadata(
new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
Everything is so wrong I can't really answer the question without fixing things.
Your nodes and containers shouldn't be controls that use OnRender. There's a lot of expectations in WPF, and one expectation is that you use their controls. If you dig into Microsoft code, they have a lot of things hard-coded for their classes.
You should have data objects for Node and Container that have Connections. Container should have a list of children Nodes.
You'll use a DataTemplate or Style to actually implement the UI. That's where you do your bindings, but don't use a multibinding. Just bind to individual values themselves. If you need to evaluate, then you create ViewModel objects that perform these calculations for you. You don't do your construction code in converters.
Because you're using bindings to connect things and your "connectable" doesn't describe whether it's a node or container or both, I'm going to assume it can be both.
For example:
public interface IConnection
{
IConnectable A { get; set; }
IConnectable B { get; set; }
}
public class Connection : IConnection, Line
{
DependencyProperty AProperty = ...;
DependencyProperty BProperty = ...;
}
public class Node : IConnectable
{
DependencyProperty ConnectionProperty = ...;
}
public class Container : IConnectable
{
DependencyProperty ConnectionProperty = ...;
ObservableCollection<IConnectable> Children = ...;
}
public class ContainerView : IConnectable
{
DependencyProperty ConnectionPointProperty = ...;
DependencyProperty ConnectionProperty = ...;
void OnSizeChanged(...)
{
RecalcConnectionPoint();
}
void OnConnectionPointOtherChanged()
{
RecalcConnectionPoint();
}
void RecalcConnectionPoint()
{
if (Connection.A == this)
{
if (Connection.B.ConnectionPoint.Left < this.Left)
{
ConnectionPoint = new Point(Left, Top + Height/2);
}
else
{
ConnectionPoint = new Point(Right, Top + Height/2);
}
}
}
}
Then you would bind the properties that match up from your Model classes to your ViewModel classes. Then manipulating the data in your Model classes would update your View.
Your Styles for your Container and Nodes would decide how to draw them, so say one day you decide a Node should look like a Rectangle instead... You change a style and don't have to dig through OnRender code.
This is how you design WPF programs.
Other benefits.
If you were to put a "Connection UI Object" somewhere on the Container, you'd bind to it's point instead. You could use a Grid to align the ConnectionPointView, and then the ConnectionPoint would be updated automatically.
I have a collection of buttons in a grid. For each one of these buttons, I want to handle the MouseEnter and MouseLeave events to animate the height of the button (and do some other interesting stuff). It all works good until I start moving my mouse too fast over and off the buttons which eventually cause the events to take place at before the other is complete. What's the best way of making sure the events wait for eachother before being triggered?
UPDATE:
Going by x0r's advice, I refactored my code into an internal class which inherits from Button and has the required methods to perform the animations. Unfortunately, this did not really solve the problem because - I think - I'm handling the Completed event of the first animation in two separate places. (correct me if I'm wrong). Here's my code:
internal class MockButton : Button
{
#region Fields
private Storyboard _mouseEnterStoryBoard;
private Storyboard _mouseLeaveStoryBoard;
private Double _width;
#endregion
#region Properties
internal Int32 Index { get; private set; }
#endregion
#region Ctors
internal MockButton(Int32 index) : this(index, 200)
{
}
internal MockButton(Int32 index, Double width)
{
this.Index = index;
this._width = width;
}
#endregion
#region Event Handlers
internal void OnMouseEnter(Action action, Double targetAnimationHeight)
{
if (_mouseEnterStoryBoard == null)
{
_mouseEnterStoryBoard = new Storyboard();
DoubleAnimation heightAnimation = new DoubleAnimation();
heightAnimation.From = 10;
heightAnimation.To = targetAnimationHeight;
heightAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));
_mouseEnterStoryBoard.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("Height"));
Storyboard.SetTarget(heightAnimation, this);
_mouseEnterStoryBoard.Children.Add(heightAnimation);
}
_mouseEnterStoryBoard.Completed += (s, e) =>
{
action.Invoke();
};
_mouseEnterStoryBoard.Begin();
}
internal void OnMouseLeave()
{
if (_mouseLeaveStoryBoard == null)
{
_mouseLeaveStoryBoard = new Storyboard();
DoubleAnimation heightAnimation = new DoubleAnimation();
heightAnimation.To = 10;
heightAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));
_mouseLeaveStoryBoard.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("Height"));
Storyboard.SetTarget(heightAnimation, this);
_mouseLeaveStoryBoard.Children.Add(heightAnimation);
}
if (_mouseEnterStoryBoard.GetCurrentState() != ClockState.Stopped)
{
_mouseEnterStoryBoard.Completed += (s, e) =>
{
_mouseLeaveStoryBoard.Begin();
};
}
else
{
_mouseLeaveStoryBoard.Begin();
}
}
#endregion
}
UPDATE 2:
Some events are getting triggered multiple times. An example of that is the Click event on the close button of my Rule object...
public Rule(Action<Int32> closeAction)
{
this.Style = Application.Current.Resources["RuleDefaultStyle"] as Style;
this.CloseAction = closeAction;
this.Loaded += (s, e) =>
{
if (_closeButton != null)
{
_closeButton.Click += (btn, args) =>
{
if (this.CloseAction != null)
{
this.CloseAction.Invoke(this.Index);
}
};
if (_closeButtonShouldBeVisible)
{
_closeButton.Visibility = System.Windows.Visibility.Visible;
}
}
};
}
And below is the Action<Int32> I'm passing to the Rule object as the CloseAction:
private void RemoveRule(Int32 ruleIndex)
{
Rule ruleToRemove = Rules.FirstOrDefault(x => x.Index.Equals(ruleIndex));
Storyboard sb = new Storyboard();
DoubleAnimation animation = new DoubleAnimation();
sb.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("Opacity"));
animation.Duration = TimeSpan.FromMilliseconds(300);
animation.From = 1;
animation.To = 0;
sb.Children.Add(animation);
Storyboard.SetTarget(animation, ruleToRemove);
sb.Completed += (s, e) =>
{
if (Rules.FirstOrDefault(x => x.Index.Equals(ruleIndex)) != null)
{
this.Rules.RemoveAt(ruleIndex);
}
};
sb.Begin();
}
UPDATE 3:
In order to avoid the animations running too early, I thought I could delay the MouseEnter event, so if the user just scrolls over the item too fast, it doesn't kick off. But I have a problem now: Say the user mouses over the item and then mouses out. If I use the Storyboard.BeginTime property, that won't safe guard against that behavior because eventhough the animation gets delayed, it's still going to start eventually... So is there a way I could prevent that from happening?
Any suggestions?
check in your mouseleave eventhandler if the first storyboard is still running and if that is the case attach the starting of the second storyboard to the Completed event of the first storybaord:
private void OnOddRowMouseLeave(object sender, MouseEventArgs e)
{
...
if(_firstStoryboard.GetCurrentState() != ClockState.Stopped)
_firstStoryboard.Completed += (s,e) => _secondStoryboard.Begin();
else
_secondStoryboard.Begin()
Everything that Silverlight does is asyncronous and so most likely what is happening is that because you are moving quickly in and out of the box the mouse leave is being fired before the mouseenter has a chance to finish. You could setup your two events so thay they have an indicator of whether or not the other is in process. For example you could do this
bool mouseOut =false;
bool mouseIn =false;
void OnMouseEnter(Action action, Double targetAnimationHeight)
{
if(!this.mouseOut)
{
this.mouseIn = true;
if (_mouseEnterStoryBoard == null)
{
_mouseEnterStoryBoard = new Storyboard();
DoubleAnimation heightAnimation = new DoubleAnimation();
heightAnimation.From = 10;
heightAnimation.To = targetAnimationHeight;
heightAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));
_mouseEnterStoryBoard.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("Height"));
Storyboard.SetTarget(heightAnimation, this);
_mouseEnterStoryBoard.Children.Add(heightAnimation);
}
_mouseEnterStoryBoard.Completed += (s, e) =>
{
action.Invoke();
};
_mouseEnterStoryBoard.Begin();
if(this.mouseOut)
{
this.OnMouseLeave();
}
this.mouseIn = false;
}
}
void OnMouseLeave()
{
if(!this.mouseIn)
{
this.mouseOut = false;
if (_mouseLeaveStoryBoard == null)
{
_mouseLeaveStoryBoard = new Storyboard();
DoubleAnimation heightAnimation = new DoubleAnimation();
heightAnimation.To = 10;
heightAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));
_mouseLeaveStoryBoard.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("Height"));
Storyboard.SetTarget(heightAnimation, this);
_mouseLeaveStoryBoard.Children.Add(heightAnimation);
}
if (_mouseEnterStoryBoard.GetCurrentState() != ClockState.Stopped)
{
_mouseEnterStoryBoard.Completed += (s, e) =>
{
_mouseLeaveStoryBoard.Begin();
};
}
else
{
_mouseLeaveStoryBoard.Begin();
}
}
else
{
this.mouseOut = true;
}
}
I haven't actually checked this code but this should help you to at least get closer to what you want. This should be quick enough that your user doesn't realize that it is not firing exactly on exit if they go over it quickly. But this should help to keep you from getting overlap.
Another way you could do this is setup the initial events as null, and have the mouse in event set the mouse in event when it is complete but the problem with that is that if the mouse out fires before the event is set then you don't event get the event firing.
I haven't implement this pattern for a while (and when I did it was in 2, as opposed to 3), and I have several examples that all seem straight forward, but I can't work out what I have done wrong in the below piece of code (The Items are not updated when the property event fires):
public partial class Index : Page
{
private IndexViewModel _vm;
public Index()
{
InitializeComponent();
_vm = new IndexViewModel(19);
this.TheDataGrid.ItemsSource = _vm.Rows;
}
public class IndexViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
this.PropertyChanged(this, e);
}
public SortableCollectionView Rows
{
get
{
return _rows;
}
set
{
if (_rows == value)
return;
_rows = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("Rows"));
}
}
This does not refresh my datagrid... as a 'hack' I have had to pass the datagrid object into my viewmodel and bind it there:
public IndexViewModel(int containerModelId, DataGrid shouldNotNeed)
{
ContainerModelId = containerModelId;
LoadOperation<vwColumn> headings = _ttasContext.Load(_ttasContext.GetRecordColumnsQuery(ContainerModelId));
headings.Completed += (sender2, e2) =>
{
//load data
LoadOperation<vwDataValue> data = _ttasContext.Load(_ttasContext.GetRecordsQuery(ContainerModelId, null));
data.Completed += (sender3, e3) =>
{
Rows = FormatData(data, headings);
shouldNotNeed.ItemsSource = Rows;
};
};
}
Assigning _vm.Rows to TheDataGrid.ItemsSource does not wire any change notification callback automatically. Try this:
in xaml:
<... x:Name=TheDataGrid ItemsSource={Binding Rows}>
In code:
this.DataContext = _vm;
As Codism points out your main problem is you need to use binding to take advantage of an INotifyPropertyChanged. However I would recommend this implementation pattern:-
public event PropertyChangedEventHandler PropertyChanged;
void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name);
}
...
set
{
if (_rows != value)
{
_rows = value;
NotifyPropertyChanged("Rows");
}
}
Note that this approach minimises the impact on a an object instance whose properties are not being observed. In the original pattern you create instances of PropertyChangedEventArgs and calls to the event delegate going off regardless of whether anything is actually listening.
this.TheDataGrid.ItemsSource = _vm.Rows
When a collection is assigned as the ItemsSource of a DataGird , any changes made to the collection can be observed by the DataGrid if the source implements INotifyCollectionChanged.
From your code sample , I can't tell if the type SortableCollectionView implements INotifyCollectionChanged or inherits from ObservableCollection.
Implementing INotifyCollectionChanged would mean that you can't reset the backing field _rows for property Rows , you can clear items in the collection and add them as needed.
Hope this helps
Are there any known issues when databinding to a control's visible property?
The control is always NOT visible regardless of what my property is.
Public ReadOnly Property IsRibbonCategory() As Boolean
Get
Return True
End Get
End Property
I tried the control's text property and other properties and they seem to work correctly.
I am trying to set a Panel's visible property.
I've found that life is better if you assume that binding to a control's Visible property is broken, despite the fact that it sometimes works. See http://support.microsoft.com/kb/327305, which says as much (and while the KB article applies to .NET 1.0 and 1.1, it still seems to be a problem in at least 2.0).
I created a utility class for creating bindings which, among other things, gave me a centralized place to add a work-around. Instead of actually creating a binding on Visible it does two things:
It subscribes to the data source's INotifyPropertyChanged.PropertyChanged event and sets the Visible value as appropriate when the event is raised.
It sets the initial value of Visible according to the current data source value.
This required a little reflection code, but wasn't too bad. It is critical that you don't bind the Visible property and do the work-around or it won't work.
Workaround: Set the Visible property on the BindingComplete event.
I had same issue setting a label's Visible property - always stays false, even though setting the Enabled property works fine.
I just hit this issue in .NET 4.7.1 and Visual Studio 2017. To fix it, I changed the Visible property on my control to be initially set to True, as I had it as False previously.
Things to check:
Be sure you've instantiated the class that has the IsRibbonCategory property
Did you set the datasource of property of the binding source to the instance of the class
The datasource update mode should be on "on validation"
Make sure you didn't set the visible property manually to false on the control
Hope that helps. Can you post more code?
A workaround would be to use a Component to databind to a control's visiblity property instead of directly binding to the control's visibility property.
See below code:
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApplication2
{
public class ControlVisibilityBinding : Component
{
private static readonly object EventControlChanged = new object();
private static readonly object EventVisibleChanged = new object();
private System.Windows.Forms.Control _control;
private bool _visible = true;
public event EventHandler VisibleChanged
{
add { Events.AddHandler(EventVisibleChanged, value); }
remove { Events.RemoveHandler(EventVisibleChanged, value); }
}
public event EventHandler ControlChanged
{
add { Events.AddHandler(EventControlChanged, value); }
remove { Events.RemoveHandler(EventControlChanged, value); }
}
public ControlVisibilityBinding()
{
}
public ControlVisibilityBinding(IContainer container)
{
container.Add(this);
}
[DefaultValue(null)]
public System.Windows.Forms.Control Control
{
get { return _control; }
set
{
if(_control == value)
{
return;
}
WireControl(_control, false);
_control = value;
if(_control != null)
{
_control.Visible = _visible;
}
WireControl(_control, true);
OnControlChanged(EventArgs.Empty);
OnVisibleChanged(EventArgs.Empty);
}
}
[DefaultValue(true)]
public bool Visible
{
get { return _visible; }
set
{
if(_visible != value)
{
_visible = value;
}
if(Control != null)
{
Control.Visible = _visible;
}
OnVisibleChanged(EventArgs.Empty);
}
}
private void WireControl(Control control, bool subscribe)
{
if(control == null)
{
return;
}
if(subscribe)
{
control.VisibleChanged += Control_VisibleChanged;
}
else
{
control.VisibleChanged -= Control_VisibleChanged;
}
}
private void Control_VisibleChanged(object sender, EventArgs e)
{
OnVisibleChanged(EventArgs.Empty);
}
protected virtual void OnVisibleChanged(EventArgs e)
{
EventHandler subscribers = (EventHandler)Events[EventVisibleChanged];
if(subscribers != null)
{
subscribers(this, e);
}
}
protected virtual void OnControlChanged(EventArgs e)
{
EventHandler subscribers = (EventHandler)Events[EventControlChanged];
if(subscribers != null)
{
subscribers(this, e);
}
}
}
static class Program
{
[STAThread]
static void Main()
{
using(Form form = new Form())
using(FlowLayoutPanel groupBoxLayoutPanel = new FlowLayoutPanel())
using(RadioButton visibleButton = new RadioButton())
using(RadioButton hiddenButton = new RadioButton())
using(GroupBox groupBox = new GroupBox())
using(Label text = new Label())
using(ControlVisibilityBinding visibilityBinding = new ControlVisibilityBinding())
using(TextBox inputTextBox = new TextBox())
{
groupBoxLayoutPanel.Dock = DockStyle.Fill;
groupBoxLayoutPanel.FlowDirection = FlowDirection.LeftToRight;
groupBoxLayoutPanel.AutoSize = true;
groupBoxLayoutPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
visibleButton.Text = "Show Label";
visibleButton.AutoSize = true;
hiddenButton.Text = "Hide Label";
hiddenButton.AutoSize = true;
groupBoxLayoutPanel.Controls.Add(visibleButton);
groupBoxLayoutPanel.Controls.Add(hiddenButton);
inputTextBox.Text = "Enter Label Text Here";
inputTextBox.Dock = DockStyle.Top;
groupBox.AutoSize = true;
groupBox.AutoSizeMode = AutoSizeMode.GrowAndShrink;
groupBox.Controls.Add(groupBoxLayoutPanel);
groupBox.Dock = DockStyle.Fill;
text.AutoSize = true;
text.ForeColor = Color.Red;
text.Dock = DockStyle.Bottom;
text.BorderStyle = BorderStyle.FixedSingle;
text.Font = new Font(text.Font.FontFamily, text.Font.Size * 1.25f, FontStyle.Bold | FontStyle.Italic);
text.DataBindings.Add("Text", inputTextBox, "Text", true, DataSourceUpdateMode.Never);
visibilityBinding.Control = text;
visibleButton.DataBindings.Add("Checked", visibilityBinding, "Visible", true, DataSourceUpdateMode.OnPropertyChanged);
Binding binding = hiddenButton.DataBindings.Add("Checked", visibilityBinding, "Visible", true, DataSourceUpdateMode.OnPropertyChanged);
ConvertEventHandler invertConverter = (sender, e) => e.Value = !((bool)e.Value);
binding.Format += invertConverter;
binding.Parse += invertConverter;
form.Controls.Add(inputTextBox);
form.Controls.Add(text);
form.Controls.Add(groupBox);
Application.Run(form);
}
}
}
}
Here is my turn around, it may be stupid but it worked many times.
I put one Panel control in my form, I make it to Fill my form and I put everything in that Panel. All the controls I bind the Visible property see their visibility change according to the objects in my DataGridView.