OxyPlot: Keep tracker open when left button released - oxyplot

I'm using OxyPlot 2014.1.546 with C# and WPF.
My plot has a custom tracker that appears when the user clicks a point. I'd like to include buttons for performing actions related to the clicked point. Adding them to the tracker template is straightforward enough; the problem is, by default, the tracker disappears as soon as the user releases the mouse button, which means it's impossible to actually click them.
Is there any way to tell OxyPlot to keep the tracker open until the user clicks outside of it?

The short answer is that OxyPlot doesn't appear to support this behavior directly. After spending some time digging through the decompiled source, I came up with the following solution, which appears to work. The basic idea was to derive my own StayOpenTrackerManipulator from OxyPlot's built-in TrackerManipulator and instantiate it in response to a click. My manipulator overrides the virtual Completed() function, which the framework calls when the mouse button is released, and defers the call to the base-class Completed(), which closes the tracker, until the next time the mouse is clicked (or until the plot is modified, or until the mouse leaves it). Since I'm using C# and WPF, I wrapped everything up in an attached behavior that can be used from XAML like so:
<PlotView behaviors:ShowTrackerAndLeaveOpenBehavior.BindToMouseDown="Left" />
but it would be simple enough to pull the guts out and reuse them in a different manner if needed. Here's the source:
/// <summary>
/// Normal OxyPlot behavior is to show the tracker when the bound mouse button is pressed,
/// and hide it again when the button is released. With this behavior set, the tracker will stay open
/// until the user clicks the plot outside it (or the plot is modified).
/// </summary>
public static class ShowTrackerAndLeaveOpenBehavior
{
public static readonly DependencyProperty BindToMouseDownProperty = DependencyProperty.RegisterAttached(
"BindToMouseDown", typeof(OxyMouseButton), typeof(ShowTrackerAndLeaveOpenBehavior),
new PropertyMetadata(default(OxyMouseButton), OnBindToMouseButtonChanged));
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static void SetBindToMouseDown(DependencyObject element, OxyMouseButton value) =>
element.SetValue(BindToMouseDownProperty, value);
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static OxyMouseButton GetBindToMouseDown(DependencyObject element) =>
(OxyMouseButton) element.GetValue(BindToMouseDownProperty);
private static void OnBindToMouseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is IPlotView plot))
throw new InvalidOperationException($"Can only be applied to {nameof(IPlotView)}");
if (plot.ActualModel == null)
throw new InvalidOperationException("Plot has no model");
var controller = plot.ActualController;
if (controller == null)
throw new InvalidOperationException("Plot has no controller");
if (e.OldValue is OxyMouseButton oldButton && oldButton != OxyMouseButton.None)
controller.UnbindMouseDown(oldButton);
var newButton = GetBindToMouseDown(d);
if (newButton == OxyMouseButton.None)
return;
controller.UnbindMouseDown(newButton);
controller.BindMouseDown(newButton, new DelegatePlotCommand<OxyMouseDownEventArgs>(
AddStayOpenTrackerManipulator));
}
private static void AddStayOpenTrackerManipulator(IPlotView view, IController controller,
OxyMouseDownEventArgs e)
{
controller.AddMouseManipulator(view, new StayOpenTrackerManipulator(view), e);
}
private class StayOpenTrackerManipulator : TrackerManipulator
{
private readonly PlotModel _plotModel;
private bool _isTrackerOpen;
public StayOpenTrackerManipulator(IPlotView plot)
: base(plot)
{
_plotModel = plot?.ActualModel ?? throw new ArgumentException("Plot has no model", nameof(plot));
Snap = true;
PointsOnly = false;
}
public override void Started(OxyMouseEventArgs e)
{
_plotModel.TrackerChanged += HandleTrackerChanged;
base.Started(e);
}
public override void Completed(OxyMouseEventArgs e)
{
if (!_isTrackerOpen)
{
ReallyCompleted(e);
}
else
{
// Completed() is called as soon as the mouse button is released.
// We won't call the base Completed() here since that would hide the tracker.
// Instead, defer the call until one of the hooked events occurs.
// The caller will still remove us from the list of active manipulators as soon as we return,
// but that's good; otherwise the tracker would continue to move around as the mouse does.
new DeferredCompletedCall(_plotModel, () => ReallyCompleted(e)).HookUp();
}
}
private void ReallyCompleted(OxyMouseEventArgs e)
{
base.Completed(e);
// Must unhook or this object will live as long as the model (instead of as long as the manipulation)
_plotModel.TrackerChanged -= HandleTrackerChanged;
}
private void HandleTrackerChanged(object sender, TrackerEventArgs e) =>
_isTrackerOpen = e.HitResult != null;
/// <summary>
/// Monitors events that should trigger manipulator completion and calls an injected function when they fire
/// </summary>
private class DeferredCompletedCall
{
private readonly PlotModel _plotModel;
private readonly Action _completed;
public DeferredCompletedCall(PlotModel plotModel, Action completed)
{
_plotModel = plotModel ?? throw new ArgumentNullException(nameof(plotModel));
_completed = completed ?? throw new ArgumentNullException(nameof(completed));
}
/// <summary>
/// Start monitoring events. Their observer lists will keep us alive until <see cref="Unhook"/> is called.
/// </summary>
public void HookUp()
{
Unhook();
_plotModel.MouseDown += HandleMouseDown;
_plotModel.Updated += HandleUpdated;
_plotModel.MouseLeave += HandleMouseLeave;
}
/// <summary>
/// Stop watching events. If they were the only things keeping us alive, we'll turn into garbage.
/// </summary>
private void Unhook()
{
_plotModel.MouseDown -= HandleMouseDown;
_plotModel.Updated -= HandleUpdated;
_plotModel.MouseLeave -= HandleMouseLeave;
}
private void CallCompletedAndUnhookEvents()
{
_completed();
Unhook();
}
private void HandleUpdated(object sender, EventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseLeave(object sender, OxyMouseEventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseDown(object sender, OxyMouseDownEventArgs e)
{
CallCompletedAndUnhookEvents();
// Since we're not setting e.Handled to true here, this click will have its regular effect in
// addition to closing the tracker; e.g. it could open the tracker again at the new position.
// Modify this code if that's not what you want.
}
}
}
}

Related

Bind to an Item of a Dependency Collection

I'm trying to create a custom control that has a header and a footer and body. The idea is that the body of the report is a custom stack panel control that will allow the user to indicate page orientation and grouping. I created a dependency property on the custom UC to accept an IList of the custom stack panel. What I am trying to do is bind to one of the stack panels in the list. But for some reason the binding is not working.
The ReportPage:
public class ReportPage : StackPanel
{
//Nothing right now but will eventually include controls for page orientation and size (8.5x11, 11x17, etc.)
}
The UserControl code behind:
public partial class Report : UserControl, INotifyPropertyChanged
{
public Report()
{
ReportPages = new List<ReportPage>();
}
public static readonly DependencyProperty =
DependencyProperty.Register("ReportPages", typeof(IList), typeof(Report));
public IList ReportPages
{
get => (IList)GetValue(ReportPagesProperty);
set
{
SetValue(ReportPagesProperty, value);
ActivePage = value[0];
OnPropertyChanged(nameof(ActivePage));
}
}
private ReportPage _activePage;
public ReportPage ActivePage
{
get => _activePage;
set
{
_activePage = value;
OnPropertyChanged(nameof(ActivePage));
}
{
}
The UserControl xaml:
<Grid>
<!--Some xaml for the header and footer.-->
<ContentControl Content="{Binding ActivePage, RelativeSource={RelativeSource, FindAncestor, AncestorType=local:Report}}"/>
</Grid>
Here is how I am consuming the custom control. This should, in my mind at least, make three "pages" which I can toggle between using a button control that I didn't share.
<reportEngine:Report>
<reportEngine:Report.ReportPages>
<reportEngine:ReportPage>
<TextBlock>This is Page 1</TextBlock>
</reportEngine:ReportPage>
<reportEngine:ReportPage>
<TextBlock>This is Page 2</TextBlock>
</reportEngine:ReportPage>
<reportEngine:ReportPage>
<TextBlock>This is Page 3</TextBlock>
</reportEngine:ReportPage>
</reportEngine:Report.ReportPages>
</reportEngine:Report>
Any Ideas why the binding isn't working?
So I at least found a quick work around. I utilized the Collection Changed Event handler pattern from this answer and modified it for static dependency properties. Then, to get the values from the collection bound to the dependency property I create a static instance of the Report object in the constructor and use that to pass various values back to the object from the collection. Something like this:
public partial class Report : UserControl, INotifyPropertyChanged
{
private static Report _thisReport;
public Report()
{
InitializeComponent();
ReportPages = new ObservableCollection<ReportPage>();
_thisReport = this;
}
public static readonly DependencyProperty ReportPagesProperty =
DependencyProperty.Register("ReportPages", typeof(IList), typeof(Report), new FrameworkPropertyMetadata(ReportPagesChanged));
public IList ReportPages
{
get => (IList)GetValue(ReportPagesProperty);
set
{
SetValue(ReportPagesProperty, value);
//Update some other properties associated with the control (Total Page Numbers, etc.)
}
}
private static void ReportPagesChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
var newColl = (INotifyCollectionChanged)eventArgs.NewValue;
if (newColl != null)
newColl.CollectionChanged += ReportPages_CollectionChanged;
var oldColl = (INotifyCollectionChanged)eventArgs.OldValue;
if (oldColl != null)
oldColl.CollectionChanged -= ReportPages_CollectionChanged;
}
private static void ReportPages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs eventArgs)
{
var newPages = (IList<ReportPage>) sender;
//Updates properties of the Report control.
_thisReport.ActivePage = newPages[0];
_thisReport.TotalPageNumber = newPages.Count;
}
}
Whether this is "correct" or not I couldn't say, but it works. If someone has a better answer I will change the answer.

Registering RoutedEventHandler in UWP template control

I'm having difficulty finding how to register a RoutedEventHandler in UWP. I'm attempting to code a template control that has event properties similar to ContentDialog's:
PrimaryButtonClick="ClickEvent"
Where ClickEvent is defined in the cs file. I'm only just getting the hang of templates, but I believe I want to do something that looks like this:
<Button Content="{TemplateBinding PrimaryButtonText}" Click="{TemplateBinding PrimaryButtonClick}"/>
Currently, all I can find is references to WPF versions of this type of code:
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged",
RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<double>),
typeof(NumericBox));
public event RoutedPropertyChangedEventHandler<double> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
private void OnValueChanged(double oldValue, double newValue)
{
RoutedPropertyChangedEventArgs<double> args =
new RoutedPropertyChangedEventArgs<double>(oldValue, newValue);
args.RoutedEvent = NumericBox.ValueChangedEvent;
RaiseEvent(args);
}
But of course the types have changed. Can someone point me in the right direction?
Unfortunately, the concept of RoutedEvent (bubbling, tunneling) is not available in UWP currently. You can just create a classic event however instead:
public event EventHandler PrimaryButtonClick;
protected void InnerButton_Click(object sender, EventArgs e)
{
PrimaryButtonClick?.Invoke( sender, e );
}
Bubbling of events is possible for some predefined events, but it is not yet possible to allow bubbling for custom events in current version of UWP.

JavaFX, List to ObservableList to ListView

So my problem is, that I have an serilized ArrayList and have to update it in my GUI to show its content in a ListView dynamically.
The serialization and deserialization works fine with the use of a DAO interface but the GUI won't refresh my ListView.
This class holds my data interaction (mostly save, load...):
public class Medienverwaltung implements Serializable, IDAO{
private static final long serialVersionUID = 1L;
private List<Medium> medienliste;
public ObservableList<Medium> obList; //public for test-reasons
public Medienverwaltung(){
medienliste = new ArrayList<Medium>();
obList = FXCollections.observableArrayList(medienliste);
}
//[...]
public List<Medium> getMedienliste(){
return this.medienliste;
}
//[...]
}
Here comes my GUI implementation snippet:
public class HauptFenster extends Application{
private Medienverwaltung medienverwaltung;
#Override
public void start(Stage stage) throws Exception{
medienverwaltung = new Medienverwaltung();
VBox root = new VBox();
ListView<String> showliste = new ListView<String>();
MenuBar menuBar = createMenuBar(stage);
root.getChildren().add(menuBar);
root.getChildren().add(showliste);
//Make Listener and refresh the shown list!
medienverwaltung.obList.addListener(new ListChangeListener<Medium>(){
#Override
public void onChanged(ListChangeListener.Change<? extends Medium> change) {
showliste.getItems().clear();
for(Medium medium : medienverwaltung.obList){
//toString() is overwritten and works, too
showliste.getItems().add(medium.toString());
}
}
});
// this adds a Medium object to the Arraylist in Medienverwaltung
medienverwaltung.aufnehmen(new Bild("Foto12", 2017, "Zuhause"));
stage.setTitle("Medien Verwaltung");
stage.setScene(new Scene(root, 800, 400) );
stage.show();
}
//[...]
I also tired to exchange the whole ArrayList from the class "Medienverwaltung" with an ObservableList, so that there is only one List remaining, which works for the GUI but not for the serialization and deserialization as I guessed before. (and tried a few other implementations)
Does anyone have an idea how to change my code so that it works?
And my second question is, what is the best way in terms of a 3 layer architecture?
The following is a reference to Fabians Answer and responds to my comment on that
Update#1.1 (addendum for explanation)
public interface IDAO {
// Save method
void speichern(List<Medium> liste) throws PersistenzException;
// Load method
List<Medium> laden() throws PersistenzException;
}
Here comes my concrete save Method:
#Override
public void speichern(List<Medium> medienliste) throws PersistenzException{
File sfile = new File("medienliste.dat");
try(FileOutputStream fos = new FileOutputStream(sfile); ObjectOutputStream oos = new ObjectOutputStream(fos)){
oos.writeObject(medienliste);
System.out.println("Serialisierung erfolgreich!");
}catch(IOException e){
e.printStackTrace();
System.out.println("Serialisierung fehlgeschlagen!");
}
}
Update#1.2 (addendum for explanation)
//[...] section of my GUI for saving
MenuItem speichern = new MenuItem("Speichern");
speichern.setOnAction(new EventHandler<ActionEvent>(){
#Override
public void handle(ActionEvent e){
try{
//Before: medienverwaltung.speichern(medienverwaltung.getMedienliste()); -> doesn't work because of serializing an ObservableList
medienverwaltung.speichern(medienverwaltung.getBackingList());
}catch(PersistenzException pe){
pe.printStackTrace();
}
}
});
//[...]
But as I guess, it's not a fine way to access the backinlist this way.
Update#2:
to respect the principle of encapsulation in a clean way I now added an overloaded Method in the class Medienverwaltung:
public void speichern() throws PersistenzException{
speichern(backingList);
}
So my GUI now only calls speichern(). This actually calls the method for saving with the backedlist which is no more accessible from the outside. I hope this is no bad coding style ^^
BTW.: If you are reading this and have a similar problem, don't use ObservableArrayList for the synchronisation with a normal List, this won't work! Use ObservableList instead.
Hide the backing list (medienliste) from other classes by removing the getter. If you modify this list using the ObservableList the ListView (or every other object that has added a listener to the list) will properly update.
Furthermore unless Medium extends Node you can simply use this kind of object as items of the ListView, since the cells set the text to the result of the toString method called for the associated item by default.
public class Medienverwaltung implements Serializable, IDAO{
private static final long serialVersionUID = 1L;
private List<Medium> backingList;
// transient field not persisted
private transient ObservableList<Medium> medienliste;
public Medienverwaltung(){
backingList = new ArrayList<Medium>();
medienliste = FXCollections.observableArrayList(backingList);
}
// make sure an ObservableList is created when reading the serialized object
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
medienliste = FXCollections.observableArrayList(backingList);
}
//[...]
public ObservableList<Medium> getMedienliste(){
return this.medienliste;
}
//[...]
}
#Override
public void start(Stage stage) throws Exception{
medienverwaltung = new Medienverwaltung();
VBox root = new VBox();
ListView<Medium> showliste = new ListView<>(medienverwaltung.getMedienliste());
MenuBar menuBar = createMenuBar(stage);
root.getChildren().add(menuBar);
root.getChildren().add(showliste);
// this adds a Medium object to the Arraylist in Medienverwaltung
medienverwaltung.aufnehmen(new Bild("Foto12", 2017, "Zuhause"));
stage.setTitle("Medien Verwaltung");
stage.setScene(new Scene(root, 800, 400) );
stage.show();
}
Note that the Medienverwaltung.aufnehmen method should not work directly with the backing list - it should use the ObservableList instead to make sure changes can be observed...
EDIT
Looking at the IDAO interface it should probably be a object different to Medienverwaltung, since otherwise you'd violate the seperation of concerns design principle; also it wouldn't make sense to pass a value as parameter that's already contained as property of the object itself.
It seems that the IDAO object should be responsible for reading/writing the list data only which would make implementing Serializable with Medienverwaltung unnecessary. Probably something like this is expected solution to your excercise:
IDAO idao = new IDAOImplementation();
Medienverwaltung medienverwaltung = new Medienverwaltung(idao.laden());
public void handle(ActionEvent e){
try{
idao.speichern(medienverwaltung.getMedienliste());
}catch(PersistenzException pe){
pe.printStackTrace();
}
}
public Medienverwaltung(List<Medium> medien) {
this.medienliste = FXCollections.observableArrayList(medien);
}
The IDAO implementation should most likely not depend on the implementation of the List and therefore not expect the List to be serializable. You can simply work around non-serialized lists by a) not using ObjectOutputStream to persist the data, but some other way not relying on serializable objects or b) simply copy the contents of the list to a serializable list:
#Override
public void speichern(List<Medium> medienliste) throws PersistenzException{
File sfile = new File("medienliste.dat");
try(FileOutputStream fos = new FileOutputStream(sfile); ObjectOutputStream oos = new ObjectOutputStream(fos)){
oos.writeObject(new ArrayList(medienliste));
System.out.println("Serialisierung erfolgreich!");
} catch(IOException e){
throw new PersistenzException(e);
}
}

SharePoint webparts requesting information prior to load

Here is how it works:
Filter web part sends row of data to all other webparts on the page.
It's control is rendered at load time, rendering the control selects which row is sent back to the other webparts on the page.
This causes the issue on the first page load where the other webparts will request the row from provider before it has finished loading and therefore has no information to provide yet.
The only solution (which is really ugly, slow and horrible) is to run all of the code that would be run in the control class the webpart uses in the webpart's constructor and use it to predict what values the control will have on the first run. This also leads to a whole bunch of issues with deploying that I really would rather avoid.
Here's the webpart code:
public class FilterProjectHeader : WebPart, IWebPartRow
{
// Visual Studio might automatically update this path when you change the Visual Web Part project item.
private const string _ascxPath = #"[link goes here]";
public DataRowView data;
public DataTable table;
private FilterProjectHeaderUserControl control;
public FilterProjectHeader()
{
//Code I want to avoid using:
//var web = SPContext.Current.Web;
//table = web.Lists["foo"].Items.GetDataTable();
//data = foo();
}
protected override void CreateChildControls()
{
control = Page.LoadControl(_ascxPath) as FilterProjectHeaderUserControl;
control.provider = this;
Controls.Add(control);
}
public PropertyDescriptorCollection Schema
{
get
{
return TypeDescriptor.GetProperties(table.DefaultView[0]);
}
}
[ConnectionProvider("Row")]
public IWebPartRow GetConnectionInterface()
{
return this;
}
public void GetRowData(RowCallback callback)
{
callback(data);
}
}
And for the control:
public partial class FilterProjectHeaderUserControl : UserControl
{
public FilterProjectHeader provider { get; set; }
private String _selectedValue;
//Both OnLoad and OnInit have the same result.
protected override void OnInit(EventArgs e)
{
//This is what gets run the first time:
if (!IsPostBack)
{
//Code here finds data then sends it back to webpart like this:
//All of the code in this method definitely does run; I have stepped
//through it and it works but it seems to happen too late to have any
//effect.
provider.data = item;
provider.table = profilesTable;
}
}
protected void filterDropDown_SelectedIndexChanged(object sender, EventArgs e)
{
//Post back method code exempted... it works.
provider.data = item;
provider.table = profilesTable;
}
So after a lot of time working with this, I found the issue is actually with what Microsoft recommends to do as best practice (they say to always use CreateChildControls to load controls onto the page).
CreateChildControls runs AFTER OnLoad when it is the first time a page is loading, but runs BEFORE OnLoad on a repost.
This is why it works on reposts, but not on first page load.
Switching CreateChildControls to OnInit solves the problem, because OnInit will always run before OnLoad.

Save option in RCP Product

Iam developing a RCP application which consists of views and editors. I can change the values and edit the values of some parameters in editor. When a value has been changed, i need to make the editor dirty as well as would also like to enable the save button. Till now, i have not implemented my save button. Could anyone guide me how to make the save button enabled as well as how can i make an editor dirty when some modifications happen in editor.
Thanks in advance. Any help will be greatly appreciated.
Regards,
Girish
Here is an overview of the Form editor logic, hop it will help you.
public class TestEditor extends FormEditor {
#Override
protected void addPages() {
// this method is called when the editor is being created
// to add the necessary pages
// page classes should be like following
// class TestEditorPage extends FormPage
try {
TestEditorPage pageTest = new TestEditorPage(this);
addPage(pageTest);
} catch (PartInitException e) {
}
}
#Override
public void doSave(IProgressMonitor monitor) {
// this method will be called on save action
// (or Ctrl + s shortcut)
}
#Override
public void doSaveAs() {
// this method will be called on save-as
//call (or Ctrl + Shift + s shortcut)
}
#Override
public boolean isSaveAsAllowed() {
// put here the call to the logic that
// check if the save is allowed
return true;
}
#Override
public boolean isDirty() {
// Here the call for the logic that
// defines if the editor is dirty or not
return true;
}
}