How to define custom Controller in XAML Plot - c#

I have 2 questions about oxyplot and since they are very simple, I put them together:
How can I define custom plot Controller in XAML? I noticed there is a Controller property in PlotView class, but there is nothing I could find in the Plot class. Am I missing something, or it's not possible at the moment?
Does maximum range property work on a DateTimeAxis? What are the units? I have tried values around hundreds or thousands, but it seems that the zoom is not limited.
My plot is defined in the XAML as follows:
<oxy:Plot>
<oxy:Plot.Axes>
<oxy:DateTimeAxis Title="Time" Position="Bottom" StringFormat="HH:mm:ss" />
<oxy:LinearAxis Title="Temperature" Position="Left" Key="Temperature" />
<oxy:LogarithmicAxis Title="Pressure" Position="Right" Key="Pressure" />
</oxy:Plot.Axes>
<oxy:Plot.Series>
<oxy:LineSeries Title="Temperature" ItemsSource="{Binding Temperatures, Mode=OneWay}" DataFieldY="Temperature" YAxisKey="Temperature" />
<oxy:LineSeries Title="Pressure" ItemsSource="{Binding Pressures, Mode=OneWay}" DataFieldY="Pressure" YAxisKey="Pressure" />
</oxy:Plot.Series>
</oxy:Plot>

Adding custom controller via XAML seems impossible, so I have used System.Windows.Interactivity.Behavior to edit plot's controller without code behind.
Behavior is defined like this:
public class CustomControllerPlotBehavior : System.Windows.Interactivity.Behavior<Plot>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
CustomizeController(AssociatedObject.ActualController);
}
private void CustomizeController(IPlotController controller)
{
controller.UnbindAll();
// actual changes to the controller
controller.BindMouseDown(OxyMouseButton.Left, OxyModifierKeys.Shift, PlotCommands.ZoomRectangle);
controller.BindMouseDown(OxyMouseButton.Left, OxyModifierKeys.Control, PlotCommands.PanAt);
controller.BindMouseDown(OxyMouseButton.Left, PlotCommands.SnapTrack);
controller.BindKeyDown(OxyKey.Home, PlotCommands.Reset);
controller.BindMouseWheel(PlotCommands.ZoomWheel);
controller.BindMouseWheel(OxyModifierKeys.Control, PlotCommands.ZoomWheelFine);
}
}
In XAML, the behavior can be used like this:
<oxy:Plot>
<!-- Other plot settings -->
<i:Interaction.Behaviors>
<behaviors:CustomControllerPlotBehavior />
</i:Interaction.Behaviors>
</oxy:Plot>

Related

MVVM Textbox gotFocus binding to a method is not working

I have a method in my view model. How I can bind this method to textbox.gotfocus property.
My XAML part is:
<TextBox Style=
"{StaticResource TextBoxHadnigPanel}"
GotFocus="{Binding GotFocusCustomerNameMethod}"
LostFocus="{Binding LostFocusCustomerNameMethod}"
x:Name="TextBoxCustomerName"
Grid.Row="0"
Grid.Column="1"
MaxLength="16"
Margin="10" />
How to bind this LostFocus and GotFocus properties?
Anyone?
Thanks in advance
You can't bind a method in WPF.
Alternative: You can use a Behavior for a TextBox with MVVM.
You need a reference to System.Windows.Interactivity to achieve this.
public class TextBoxFocusBehavior : Behavior<TextBox>
{
#region Overrides of Behavior
protected override void OnAttached()
{
AssociatedObject.GotFocus += AssociatedObject_GotFocus;
AssociatedObject.LostFocus += AssociatedObject_LostFocus;
base.OnAttached();
}
private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
{
//TODO Your LostFocus Method here.
}
private void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
{
//TODO Your GotFocus Method here.
}
#endregion
}
Xaml:
You need a reference in the xaml file:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behaviors="clr-namespace:YourNamespace"
<TextBox Style="{StaticResource TextBoxHadnigPanel}"
x:Name="TextBoxCustomerName"
Grid.Row="0"
Grid.Column="1"
MaxLength="16"
Margin="10">
<i:Interaction.Behaviors>
<behaviors:TextBoxFocusBehavior />
</i:Interaction.Behaviors>
</TextBox>

Binding slider ValueChanged event with Xamarin.Forms

I have a slider on a page, and I am trying to implement a step so that the slider only allows integer values.
In order to achieve that, I am trying to associate an event handler to the ValueChanged event. Here is my XAML code:
<Slider ValueChanged="OnSliderValueChanged" Maximum="5" Minimum="1" Value="{Binding MaxProductsToOffer}"/>
And my event handler:
public void OnSliderValueChanged(object sender, EventArgs e)
I get the following compilation error:
Position 15:21. EventHandler "OnSliderValueChanged" not found in type
"MyApp.Views.SettingsPage"
What am I missing?
A slider in Xamarin Forms has explicit ValueChangedEventArgs so your event handler should look like this
public void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
https://learn.microsoft.com/en-us/dotnet/api/xamarin.forms.slider.valuechanged?view=xamarin-forms
Add the method:
void OnSliderValueChanged(object sender, EventArgs e)
{
...
}
Add it in code behind.
You do not need your event handled.
Position 15:21. EventHandler "OnSliderValueChanged" not found in type
"MyApp.Views.SettingsPage"
Means that you don't have an EvenHandler with the name "OnSliderValueChanged" on MyApp.Views.SettingsPage. Where it should be since you pointing to it in XAML -> ValueChanged="OnSliderValueChanged".
So either define it in your page or remove it from XAML.
Regarding your problem, since you are using bindings Value="{Binding MaxProductsToOffer}" I assume that you are following the MVVM pattern in your project and that you have a dedicated ViewModel bound to MyApp.Views.SettingsPage. If so you all you have to do is to bind Slider.Value to an integer property in your ViewModel that will automatically handle the conversion for you. Here is a solution that does not require any code behind:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SliderTest"
x:Class="SliderTest.MyPage"
Padding="50">
<ContentPage.BindingContext>
<local:MainViewModel />
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Slider x:Name="slider" Maximum="5" Minimum="1" Value="{Binding SliderValue}" />
<Label Text="{Binding Source={x:Reference Name=slider}, Path=Value}" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
public class MainViewModel : INotifyPropertyChanged
{
int sliderValue;
public int SliderValue
{
get => sliderValue;
set
{
sliderValue = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Alternatively you could: extend Slider and use a private EventHandler to manipulate the value or use an EventHandler in your page. Code example can be found here. However I find it as an overkill compare to my original proposal.
P.S.: If you are using MVVM prefer solutions that does not involve writing code in the UI layer unless it is really necessary.

x:Bind ViewModel method to an Event inside DataTemplate

I'm basically asking the same question as this person, but in the context of the newer x:Bind.
ViewModels' DataContext is defined like so
<Page.DataContext>
<vm:ChapterPageViewModel x:Name="ViewModel" />
</Page.DataContext>
So whenever I need to bind something I do it explicitely to the ViewModel like so
ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}"
However that doesn't work within templates
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}"> <-- this here is the culprit
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
Reading the documentation, I found that using Path should basically reset the context to the page, but this (x:Bind Path=ViewModel.PageResizeEvent didn't work either. I'm still getting Object reference not set to an instance of an object, which should mean that it doesn't see the method (but a null).
Image class:
public class Image {
public int page { get; set; }
public string url { get; set; }
public int width { get; set; }
public int heigth { get; set; }
}
And in the ChapterPageViewModel
private List<Image> _pageList;
public List<Image> pageList {
get { return _pageList; }
set { Set(ref _pageList, value); }
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
IDictionary<string, object> suspensionState)
{
Initialize();
await Task.CompletedTask;
}
private async void Initialize()
{
pageList = await ComicChapterGet.GetAsync(_chapterId);
}
public void PageResized(object sender, SizeChangedEventArgs e)
{
//resizing logic happens here
}
We have two problems here:
First, trying to directly bind an event to a event handler delegate
That will never work, simply put.
One way to handle an event on MVVM pattern is by using EventTrigger and ICommand.
It requires a class that implements ICommand. This post will help you if don't know how to do it. I'll call mine DelegateCommand.
Here's how I would refactor it in two steps:
1) Add a Command to the VM:
public class ChapterPageViewModel
{
public ChapterPageViewModel()
{
this.PageResizedCommand = new DelegateCommand(OnPageResized);
}
public DelegateCommand PageResizedCommand { get; }
private void OnPageResized()
{ }
}
2) Bind that Command to the SizeChanged event with EventTrigger and InvokeCommandAction.
<Page (...)
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core">
(...)
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" >
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{x:Bind ViewModel.PageResizedCommand }" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
</Page>
"But Gabriel", you say, "that didn't work!"
I know! And that's because of the second problem, which is trying to x:Bind a property that does not belong to the DataTemplate class
This one is closely related to this question, so I´ll borrow some info from there.
From MSDN, regarding DataTemplate and x:Bind
Inside a DataTemplate (whether used as an item template, a content
template, or a header template), the value of Path is not interpreted
in the context of the page, but in the context of the data object
being templated. So that its bindings can be validated (and efficient
code generated for them) at compile-time, a DataTemplate needs to
declare the type of its data object using x:DataType.
So, when you do <ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}">, you're actually searching for a property named ViewModel on the that models:Image class, which is the DataTemplate's x:DataType. And such a property does not exist on that class.
Here, I can see two options. Choose one of them:
Add that ViewModel as a property on the Image class, and fill it up on the VM.
public class Image {
(...)
public ChapterPageViewModel ViewModel { get; set; }
}
public class ChapterPageViewModel
{
(...)
private async void Initialize() {
pageList = await ComicChapterGet.GetAsync(_chapterId);
foreach(Image img in pageList)
img.ViewModel = this;
}
}
With only this, that previous code should work with no need to change anything else.
Drop that x:Bind and go back to good ol'Binding with ElementName.
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" x:Name="flipView">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{Binding DataContext.PageResizedCommand
, ElementName=flipView}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
This one kind of defeat the purpose of your question, but it does work and it's easier to pull off then the previous one.

Caliburn.Micro attach event handler on event of Behavior

I wrote my own Behavior to handle a swipe gesture and put it to the ItemTemplate of a ListView. If a swipe is completed, I raise an event for LeftSwipe or RightSwipe. This event should be handled by my ViewModel.
I use the syntax of Caliburn.Micro to attach handler to an event: cm:Message.Attach="[Event LeftSwipe] = [LeftSwipe($source, $eventArgs)".
This is my Behavior:
public class SwipeInteractionBehavior : DependencyObject, IBehavior
{
public DependencyObject AssociatedObject { get; private set; }
public void Attach(DependencyObject associatedObject)
{
// ...
}
public void Detach()
{
// ...
}
public event EventHandler LeftSwipe;
public event EventHandler RightSwipe;
// ...
// ...
private void OnLeftSwipe(FrameworkElement element)
{
// ...
if (LeftSwipe != null)
{
LeftSwipe(this, EventArgs.Empty);
}
}
private void OnRightSwipe(FrameworkElement element)
{
// ...
if (RightSwipe != null)
{
RightSwipe(this, EventArgs.Empty);
}
}
}
This is how I use this Behavior inside of ListViews ItemTemplate:
<ListView x:Name="Links" IsItemClickEnabled="True" SelectionMode="None" cm:Message.Attach="[Event ItemClick] = [Click($source, $eventArgs)]">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid>
<Border x:Name="todoItem" Loaded="Border_Loaded" Background="White">
<i:Interaction.Behaviors>
<local:SwipeInteractionBehavior cm:Message.Attach="[Event LeftSwipe] = [LeftSwipe($source, $eventArgs)]" />
</i:Interaction.Behaviors>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Title}" Style="{ThemeResource ListViewItemContentTextBlockStyle}" />
<TextBlock Text="{Binding Url}" Style="{ThemeResource ListViewItemSubheaderTextBlockStyle}" />
<TextBlock Text="{Binding Tags, Converter={StaticResource ListToString}}" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I ran in an exception if I raise the LeftSwipe event, this is my StackTrace:
System.Exception was not handled.
HResult=-2146233088
Message=No target found for method LeftSwipe.
Source=Caliburn.Micro.Platform
StackTrace:
at Caliburn.Micro.ActionMessage.Invoke(Object eventArgs)
at Caliburn.Micro.TriggerAction`1.Execute(Object sender, Object parameter)
at Microsoft.Xaml.Interactivity.Interaction.ExecuteActions(Object sender, ActionCollection actions, Object parameter)
at Microsoft.Xaml.Interactions.Core.EventTriggerBehavior.OnEvent(Object sender, Object eventArgs)
at ReaderApp.SwipeInteractionBehavior.<>c__DisplayClass5.<OnLeftSwipe>b__4()
at ReaderApp.Extensions.FrameworkElementExtension.<>c__DisplayClass2.<Animate>b__0(Object s, Object e)
InnerException:
ViewModel:
public class MainViewModel : ViewModelBase
{
private readonly IEventAggregator eventAggregator;
private readonly Database database;
public BindableCollection<Link> Links
{
get;
private set;
}
public MainViewModel(INavigationService navigationService, IEventAggregator eventAggregator, Database database)
: base(navigationService)
{
this.eventAggregator = eventAggregator;
this.database = database;
Links = new BindableCollection<Link>();
}
public async void LeftSwipe(object sender, EventArgs e)
{
// ...
}
public void RightSwipe(object sender, EventArgs e)
{
// ...
}
}
So the problem is that ActionMessage inherits TriggerAction<FrameworkElement> which means it can't attach correctly to SwipeInteractionBehavior.
It's also complicated by the fact there's some major API differences between the WPF / Silverlight / Windows Phone 8 Interactivity SDK and the WinRT Interactivity SDK. You can see a bit of what I mean in the Parser comments.
What I'd recommend is implementing SwipeInteractionBehavior as a Trigger Behaviour much like EventTriggerBehavior, this used to be a separate base class but with WinRT it's still IBehavior, the difference is that it has a property called Actions of type ActionCollection. Stupidly there is no interface is base class enforcing this so Caliburn.Micro is stuck making some assumptions.
You'd then use Interaction.ExecuteActions to trigger those actions, in the end your xaml should look something like.
<Border x:Name="SwipeTarget">
<i:Interaction.Behaviors>
<local:SwipeEventBehavior Direction="Left">
<cm:ActionMessage AssociatedObject="{Binding ElementName=SwipeTarget" Method="LeftSwipe" />
</local:SwipeEventBehavior>
</i:Interaction.Behaviors>
</Border>
It's a bit more long winded, but we need to work around the limitations imposed by the changes in the SDK.

CallMethodAction doesn't execute code-behind methods in Windows Phone 8.1

I created a simple XAML page:
<Page.DataContext>
<local:MainPageViewModel />
</Page.DataContext>
<Interactivity:Interaction.Behaviors>
<Core:DataTriggerBehavior Binding="{Binding MyNumber}" Value="3">
<Core:CallMethodAction MethodName="TestMethod" TargetObject="{Binding ElementName=page}" />
<Core:CallMethodAction MethodName="ViewModelMethod" TargetObject="{Binding Mode=OneWay}" />
</Core:DataTriggerBehavior>
</Interactivity:Interaction.Behaviors>
<Grid>
<TextBox
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding MyNumber,
Mode=TwoWay}"
TextWrapping="Wrap" />
</Grid>
And a ViewModel for this page:
public class MainPageViewModel : INotifyPropertyChanged
{
private int _myNumber;
public int MyNumber
{
get { return _myNumber; }
set
{
_myNumber = value;
RaisePropertyChanged("MyNumber");
Debug.WriteLine("Property MyNumber changed.");
}
}
public void ViewModelMethod()
{
Debug.WriteLine("ViewModelMethod called.");
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion INotifyPropertyChanged
}
In the page code behind, I added the following method:
public void TestMethod()
{
Debug.WriteLine("Method TestMethod called.");
}
When I enter number 3 in the TextBox, only the second CallMethodAction gets executed.
The TextMethod in the code behind doesn't execute. Why? I never had this problem in Windows Phone 8.
Found a solution! In Windows Phone 8.0, you have to set the TargetObject property to call methods in the code behind. In Windows Phone 8.1, you don't set the TargetObject at all. The code that works is:
<Core:CallMethodAction MethodName="TestMethod" />
Though, I still think it should at least throw an error, if a method doesn't exist...
This works for me, Windows Phone 8.1...
XAML page
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="Tapped">
<Core:CallMethodAction MethodName="TestMethod" TargetObject="{Binding ElementName=page}"/>
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
Is base on Tappe event, but I think in will works on your case.
For the method, important! make it public.
public void TestMethod(object sender, TappedRoutedEventArgs e)
{
Debug.WriteLine("TestMethod");
}

Categories

Resources