is it possible to define (not switch) VisualStates in CodeBehind?
I'm creating an Adorner, that draws some rectangles in OnRender. What I'd like to do is to change the Opacity of these Rectangles by it's Property IsMouseOver (say from 0.3 to 0.8).
In any control with a visual tree I'd add some VisualStates and switch those with a DataStateBehavior. How do I do this with an Adorner?
this is entirely possible.
if anyone is interested here is how I did it:
public class MyAdorner: Adorner
{
ctor (...):base(...)
{
...
var storyboard = new Storyboard();
var doubleAnimation = new DoubleAnimation(0.2,new Duration(TimeSpan.Zero));
Storyboard.SetTarget(doubleAnimation,this);
Storyboard.SetTargetProperty(doubleAnimation,new PropertyPath(RectOpacityProperty));
storyboard.Children.Add(doubleAnimation);
var storyboard2 = new Storyboard();
var doubleAnimation2 = new DoubleAnimation(0.5, new Duration(TimeSpan.Zero));
Storyboard.SetTarget(doubleAnimation2, this);
Storyboard.SetTargetProperty(doubleAnimation2, new PropertyPath(RectOpacityProperty));
storyboard2.Children.Add(doubleAnimation2);
var stateGroup = new VisualStateGroup { Name = "MouseOverState" };
stateGroup.States.Add(new VisualState { Name = "MouseOut", Storyboard = storyboard });
stateGroup.States.Add(new VisualState { Name = "MouseOver", Storyboard = storyboard2});
var sgs = VisualStateManager.GetVisualStateGroups(this);
sgs.Add(stateGroup);
var dsb = new DataStateBehavior
{
Value = true,
FalseState = "MouseOut",
TrueState = "MouseOver"
};
BindingOperations.SetBinding(dsb, DataStateBehavior.BindingProperty, new Binding {Source = this, Path = new PropertyPath(IsMouseOverProperty)});
dsb.Attach(this);
}
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(_mouseOverBrush, _pen, _rects[i]); //mouseoverbrush is a Solidcolorbrush
}
public double RectOpacity
{
get { return (double)GetValue(RectOpacityProperty); }
set { SetValue(RectOpacityProperty, value); }
}
public static readonly DependencyProperty RectOpacityProperty =
DependencyProperty.Register("RectOpacity", typeof(double), typeof(XmlNodeWrapperAdorner), new FrameworkPropertyMetadata(0.0,FrameworkPropertyMetadataOptions.AffectsRender,(o, args) =>
{
var adorner = o as MyAdorner;
adorner._mouseOverBrush.Color = Color.FromArgb((byte)((double)args.NewValue * 0xFF), 0xFF, 0xBE, 0x00);
}));
}
pretty straightforward actually.
key points here are:
you cannot set the VisualStateGroups attached property. you have to get the collection and then add your own group
you cannot do new DataStateBehavior{Binding = new Binding(...){...}} as this will assign not bind some value to the property. As Behvior<T> doesn't derive from FrameworkElement you also can't use SetBinding but have to use the BindingOperations class.
for automatic rerendering when the property changes keep in mind to set FrameworkPropertyMetadataOptions.AffectsRender.
Since you're already creating a custom adorner with your own behavior, i would suggest that you override the MouseOver method of the adorner and change the opacity of your rectangles there...
another way would be to listen to your own PropertyChanged event and monitor the change in IsMouseOver, or maybe monitor the MouseMove event...
If you could add States in code, tools such as Blend would have to run all code in all possible configurations to find out what states are present/possible.
So, no, you can't do this in code. It only possible using attributes.
EDIT
I stand corrected but the problem mentioned still remains. This technique is not useful for designers.
Related
I have a problem with running Storyboard for dynamically created UserControl in .net WPF.
These are examples of my classes:
class EventsPage {
// ...
public void AddEvent(Event #event) {
var eventUC = new EventUserContrl(#event);
eventUC.ExpandCollapseAnimation += ExpandCollapseAnimation;
EventsStackPanel.Children.Add(eventUC);
}
private ExpandCollapseAnimation(EventUserControl eventUC, double height, double time) {
// Create frames using custom functions for creating them.
var frames = new DoubleKeyFrameCollection() {
StoryBoardsBuilder.CreateEasingDoubleKeyFrame(0.0, eventUC.ActualHeight),
StoryBoardsBuilder.CreateEasingDoubleKeyFrame(time, destinationHeight)
};
// Create Animation.
var heightSizeAnimation= StoryBoardsBuilder.BuildDoubleAnimationUsingKeyFrames(
FillBehavior.Stop, frames, eventUC.Name, new PropertyPath("Height"));
// Create StoryBoard.
var storyboard = new Storyboard();
// Add Animations into StoryBoard.
storyboard.Children.Add(heightSizeAnimation);
// Create final function.
storyboard.Completed += (sender, e) => {
eventUC.Height = destinationHeight;
};
// Run animation.
storyboard.Begin(this);
}
// ...
}
And after launching it, at storyboard.Begin(this), an exception is shown:
System.InvalidOperationException: „Name „” cannot be found in the namespace „ProjectName.Pages.EventsPage ”.”
I did something like this but for manually placed user controls in page, and it works, but this won't.
This is StoryBuilder code:
public static EasingDoubleKeyFrame CreateEasingDoubleKeyFrame(
double frameTimeInSeconds,
double value) {
// Create double key frame.
return new EasingDoubleKeyFrame() {
KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(frameTimeInSeconds)),
Value = value
};
}
public static DoubleAnimationUsingKeyFrames BuildDoubleAnimationUsingKeyFrames(
FillBehavior fillBehavior,
DoubleKeyFrameCollection keyFrames,
string targetName,
PropertyPath targetProperty) {
// Create animation.
var animation = new DoubleAnimationUsingKeyFrames();
// Set animation end behavior.
animation.FillBehavior = fillBehavior;
// Set animation frames.
animation.KeyFrames = keyFrames;
// Set animation target object.
Storyboard.SetTargetName(animation, targetName);
// Set animation target property.
Storyboard.SetTargetProperty(animation, targetProperty);
return animation;
}
It looks like you are setting the target of the Storyboard wrong.
Storyboard.SetTargetName expects a registered element name.
You have two options: either use Storyboard.SetTarget or register the control's name and use Storyboard.SetTargetName.
Solution 1: Storyboard.SetTarget (recommended)
The recommended approach is to use Storyboard.SetTarget, when creating the animation in C# (instead of the more convenient XAML). Storyboard.SetTargetName expects an element within a XAML namescope, having the FrameworkElement.Name set using the X:Name directive. Using Storyboard.SetTarget eliminates the requirement of the target element to be registered within a name scope.
StoryBoardBuilder.cs
class StoryBoardBuilder
{
public static DoubleAnimationUsingKeyFrames BuildDoubleAnimationUsingKeyFrames(
FillBehavior fillBehavior,
DoubleKeyFrameCollection keyFrames,
DependencyObject target,
PropertyPath targetProperty)
{
// Create animation.
var animation = new DoubleAnimationUsingKeyFrames();
// Set animation end behavior.
animation.FillBehavior = fillBehavior;
// Set animation frames.
animation.KeyFrames = keyFrames;
// Set animation target object.
Storyboard.SetTarget(animation, target);
// Set animation target property.
Storyboard.SetTargetProperty(animation, targetProperty);
return animation;
}
}
Example
class EventsPage
{
// ...
public void AddEvent(Event #event) {
var eventUC = new EventUserContrl(#event);
eventUC.ExpandCollapseAnimation += ExpandCollapseAnimation;
}
private ExpandCollapseAnimation(EventUserControl eventUC, double height, double time)
{
...
// Create Animation.
var heightSizeAnimation= StoryBoardBuilder.BuildDoubleAnimationUsingKeyFrames(
FillBehavior.Stop,
frames,
eventUC,
new PropertyPath("Height"));
...
}
// ...
}
Solution 2: Storyboard.SetTargetName
Storyboard.SetTargetName expects a named FrameworkElement as animation target. The name of this element must be registered within the current XAML name scope. This is done automatically when naming elements in XAML using the x:Name directive.
Since you have decided to create the elements in C# you have to register the element name manually. In case there is no active name scope, you also have to create a new name scope manually.
The easiest to get access to an existing name scope is the reference of a named XAML element. On this element you call FrameworkElement.RegisterName to register an element. See Microsoft Docs: Targeting Framework Elements, Framework Content Elements, and Freezables to learn more.
StoryBoardBuilder.cs
class StoryBoardBuilder
{
public static DoubleAnimationUsingKeyFrames BuildDoubleAnimationUsingKeyFrames(
FillBehavior fillBehavior,
DoubleKeyFrameCollection keyFrames,
string targetName,
PropertyPath targetProperty)
{
// Create animation.
var animation = new DoubleAnimationUsingKeyFrames();
// Set animation end behavior.
animation.FillBehavior = fillBehavior;
// Set animation frames.
animation.KeyFrames = keyFrames;
// Set animation target object.
Storyboard.SetTargetName(animation, targetName);
// Set animation target property.
Storyboard.SetTargetProperty(animation, targetProperty);
return animation;
}
}
Example
class EventsPage
{
// ...
public void AddEvent(Event #event)
{
var eventUC = new EventUserContrl(#event);
// Name the element
eventUC.Name = "MyEventUserContrl";
// Register the element name in the current name scope manually
// using an existing named element
EventsStackPanel.RegisterName(eventUC.Name, eventUC);
EventsStackPanel.Children.Add(eventUC);
eventUC.ExpandCollapseAnimation += ExpandCollapseAnimation;
}
private ExpandCollapseAnimation(EventUserControl eventUC, double height, double time)
{
...
// Create Animation.
var heightSizeAnimation= StoryBoardBuilder.BuildDoubleAnimationUsingKeyFrames(
FillBehavior.Stop,
frames,
eventUC.Name,
new PropertyPath("Height"));
...
}
// ...
}
I have a custom control dynamically created by another control, I want to change its VisualState based on the VisualState of the parent.
VisualState is a DependencyProperty that accept an enumerator, the control internally uses it inside the OnPropertyChange event to change size and internal layout.
The property are made identical on both controls (of course except the type).
public ControlSize VisualState
{
get { return (ControlSize)GetValue(VisualStateProperty); }
set
{
if (value != VisualState)
{
SetValue(VisualStateProperty, value);
}
}
}
public static readonly DependencyProperty VisualStateProperty = DependencyProperty.RegisterAttached(nameof(VisualState), typeof(ControlSize), typeof(CountersListControl), new PropertyMetadata(ControlSize.Large, OnVisualStateChanged));
The parent control dynamically allocate the component and binds its VisualState to the new control VisualState:
CounterControl cc = new CounterControl();
cc.SetBinding(CounterControl.ValueProperty, new Binding() { Path = new PropertyPath(nameof(Counter.Amount)), Source = counter, Mode = BindingMode.TwoWay });
//cc.DataContext = this;//I tried with it, but it doesn't change a thing
cc.SetBinding(CounterControl.VisualStateProperty, new Binding() { Path = new PropertyPath(nameof(VisualState)), Source = this, Mode = BindingMode.OneWay });
The Value property binds without any issue to Counter.Amount, and looks that VisualState does too.
BUT the OnVisualState method is called when the parent is changed, while the children value is not.
UPDATE: I debugged the binding as suggested by #EdPlunkett, and I was getting the following message:
Error: Converter failed to convert value of type 'Windows.Foundation.Int32' to type 'ControlSize';
ControlSize is an enumerable, so it should be able to convert it.
This happens because somehow it can't convert an Int32 into an enumerable (even if the source is the same enumerable).
I solved creating an IValueConverter that converts Int32/ControlSize types and assigning it to the binding.
Binding visualStateBinding = new Binding() { Path = new PropertyPath(nameof(VisualState)), Source = this, Mode = BindingMode.OneWay, Converter = new ControlSizeConverter() };
I have a problem with a ListView. I want each Cell to have a label and a switch but the text of the label does not appear.
Here is my code:
public class FilterPage : ContentPage
{
public FilterPage()
{
List<FilterCell> listContent = new List<FilterCell>();
foreach(string type in Database.RestaurantTypes)
{
FilterCell fc = new FilterCell();
fc.Text = type;
listContent.Add(fc);
}
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
types.ItemsSource = listContent;
var layout = new StackLayout();
layout.Children.Add(types);
Content = layout;
}
}
public class FilterCell : ViewCell
{
private Label label;
public Switch CellSwitch { get; private set; }
public String Text{ get { return label.Text; } set { label.Text = value; } }
public FilterCell()
{
label = new Label();
CellSwitch = new Switch();
var layout = new StackLayout
{
Padding = new Thickness(20, 0, 0, 0),
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = { label, CellSwitch }
};
View = layout;
}
}
If I enter a fixed Text in the FilterCell-Constructor it works fine (e.g.: label.Text = "Hello World")
When I create a Method for the ItemSelected-Event and read out the SelectedItem.Text Property I get the text I assigned as Value but it's never displayed. Only the switch is displayed when I try to run this Code.
Thanks for your help
Niko
Ohh boy. This code looks like a rape (sorry I had to say this).
Now let's see what's wrong:
The reason is you are mixing up data and view heavily.
The line
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
means: "For each item in the list (ItemsSource) create a new filter cell". The FilterCells that you create in the loop are never displayed.
The easy fix
public class FilterPage : ContentPage
{
public FilterPage()
{
var restaurantTypes = new[] {"Pizza", "China", "German"}; // Database.RestaurantTypes
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(() =>
{
var cell = new SwitchCell();
cell.SetBinding(SwitchCell.TextProperty, ".");
return cell;
});
types.ItemsSource = restaurantTypes;
Content = types;
}
}
There is a standard cell type that contains a label and a switch SwitchCell, use it.
As ItemsSource of your list, you have to use your data. In your case the list of restaurant types. I just mocked them with a static list.
The DataTemplate creates the SwitchCell and sets the Databinding for the Text property. This is the magic glue between View and data. The "." binds it to the data item itself. We use it, because our list contains items of strings and the Text should be exactly the string. (read about Databinding: https://developer.xamarin.com/guides/xamarin-forms/getting-started/introduction-to-xamarin-forms/#Data_Binding )
I striped away the StackLayout that contained the list. You can directly set the list as Content of the page.
Lesson
use standard controls, if possible
You should always try to remember to keep data and view apart from each other and use data binding to connect to each other.
Try to avoid unnecessary views.
Basically I want to make functionality, when I input text to my Editor it will appear inserted data to my label. And if I will swipe page to another page, that data should be bind'ed to that label in previous page where I entered data.
So I have portable class. In that class I have method public ContentPage CreatePage(MyObject thing) here I define many Labels, boxes , buttons and etc. But I will indicate most important things: Here I am define my Label and Editor:
public partial class CoolPage: CarouselPage
{
public CoolPage()
{
foreach (MyObject p in things)
{
Children.Add(CreatePage(p));
}
}
public ContentPage CreatePage(MyObject thing) {
var emptyLabel = new Label
{
Text = "Text",
WidthRequest = 50,
HeightRequest = 50,
BackgroundColor = Color.White
};
((StackLayout)page.Content).Children.Add(emptyLabel);
var inputNumb = new Editor
{
Text=thing.Number,
TextColor = Color.Black,
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill,
IsVisible = true,
BackgroundColor = Color.White
};
inputNumb.SetBinding(Label.TextProperty, "Text");
inputNumb.BindingContext = thing.Number;
((StackLayout)page.Content).Children.Add(inputNumb);
}
}
I have tried to impelemnt such a event:
inputNumb.Completed += (sender, args) =>
{
inputNumb.SetBinding(Label.TextProperty, "Text");
inputNumb.BindingContext = thing.Number;
};
but it is not working. And I think because it is on same method. Also I tried to do out of method scope, by implementing such a line on CreatePage method inputCarNumb.Completed += InputCarNumb_Completed; But then when you define your variable inputNumb it doesn't recognize and I don't know how to implement in other case. I know it is very simple, but I think I miss something by doing SetBinding / BindingContext .
I solved this problem like this:
emptyLabel.SetBinding(Label.TextProperty, "Text");
emptyLabel.BindingContext = inputNumb;
Make sure your MyObject inherits from and implements INotifyPropertyChanged so that PropertyChanged fires whenever Number changes. I generally inherit from XLabs's ViewModel, and use their SetProperty method. Don't bother setting the binding in the event. But the 2nd parameter of SetBinding should be "Number" which is MyObject's property name. Also the BindingContext should = thing.
I have a uvSelfLoadingTextBox with multiple instances on a form.
I would like to load the tooltip with the _value property at run time.
I've tried
public ucSelfLoadingTextBox()
{
Windows.Forms.ToolTip myToolTip;
myToolTip.AutomaticDelay = 5000;
myToolTip.AutoPopDelay = 50000;
myToolTip.InitialDelay = 100;
myToolTip.ReshowDelay = 500;
myToolTip.SetToolTip(this, _value);
inside the control but that does not work.
I have tried using the tooltip that is dragged onto the form
ucSelfLoadingLogicTextBox uc = new ucSelfLoadingLogicTextBox();
toolTipA.SetToolTip(uc,uc._value );
and that does not work.
What is the correct way to do this?
You forgot to instantiate myToolTip. You need to set it to new Tooltip().
Also, I don't think it's a good practice to assign the tooltip in the textbox's constructor. You could do this in OnCreateControl() (that you need to override).
Your code could therefore become:
protected override void OnCreateControl()
{
base.OnCreateControl();
var myToolTip = new System.Windows.Forms.ToolTip
{
AutomaticDelay = 5000,
AutoPopDelay = 50000,
InitialDelay = 100,
ReshowDelay = 500
};
myToolTip.SetToolTip(this, this.Text);
}
Many visible controls on windows form have ToolTip property. Just set the Tooltip with your newly created one. You can also add tooltip to your form. Have you tried this?
myToolTip.ShowAlways = true;
And try to set this tip to a button control. This may be a good test for your tooltip.