How should one implement a change state dialog with undo/redo support in Catel? - catel

I cannot get Undo and Redo to behave correctly when using a dialog.
I have a simple model with a property indicating the state of the object(running, paused, stopped) which can be altered via a dialog. What happens is that I get actions that seems to do nothing in my undo queue or undo restores the object to an intermediate state.
The model object is registered with memento in the constructor. The dialog has three radio buttons each representing one of the three different states. Each radio button is bind to a command each. Each command performs a change of the property. I have tried two different approaches, either each command sets the property directly in the object or each command sets an instance variable for the view model when called and then I use the Saving event to modify the object.
If using the first approach each property change is put on the Undo queue if the user clicks on more than just one radiobutton before clicking Ok in the dialog. Tried to solve that by wrapping the whole dialog into a batch but that results in undoing the state change the object is restored to the state it had before the final one, i.e. if the property was set to stopped before the dialog opened and the user pressed the pause radiobutton, then start one and finally Ok, undo will set the property to paused instead of the expected stopped.
If using the second approach the user opens the dialog, change the state to paused, click Ok in the dialog the undo/redo behaves as expected but if the dialog is opened again and Cancel is chosen one more action is added to the Undo queue, i.e. the user has to click Undo twice to get back to the initial stopped-state.
So my question is how should this be correctly implemented to get the expected behaviour; that each dialog interaction can be undone and not every interaction in the dialog?
Here is the code for the ViewModel:
namespace UndoRedoTest.ViewModels
{
using Catel.Data;
using Catel.MVVM;
public class StartStopViewModel : ViewModelBase
{
Machine.MachineState _state;
public StartStopViewModel(Machine controlledMachine)
{
ControlledMachine = controlledMachine;
_state = controlledMachine.State;
StartMachine = new Command(OnStartMachineExecute);
PauseMachine = new Command(OnPauseMachineExecute);
StopMachine = new Command(OnStopMachineExecute);
Saving += StartStopViewModel_Saving;
}
void StartStopViewModel_Saving(object sender, SavingEventArgs e)
{
ControlledMachine.State = _state;
}
[Model]
public Machine ControlledMachine
{
get { return GetValue<Machine>(ControlledMachineProperty); }
private set { SetValue(ControlledMachineProperty, value); }
}
public static readonly PropertyData ControlledMachineProperty = RegisterProperty("ControlledMachine", typeof(Machine));
public override string Title { get { return "Set Machine state"; } }
public Command StartMachine { get; private set; }
public Command PauseMachine { get; private set; }
public Command StopMachine { get; private set; }
private void OnStartMachineExecute()
{
_state = Machine.MachineState.RUNNING;
//ControlledMachine.SecondState = Machine.MachineState.RUNNING;
}
private void OnPauseMachineExecute()
{
_state = Machine.MachineState.PAUSED;
//ControlledMachine.SecondState = Machine.MachineState.PAUSED;
}
private void OnStopMachineExecute()
{
_state = Machine.MachineState.STOPPED;
//ControlledMachine.SecondState = Machine.MachineState.STOPPED;
}
}
}

First of all, don't subscribe to the Saving event but simply override the Save() method. Note that Catel handles the model state for you when you decorate a model with the ModelAttribute. Therefore you need to get the prestate and poststate of the dialog and then push the result set into a batch.
For example, I would create extension methods for the object class (or model class) like this:
public static Dictionary<string, object> GetProperties(this IModel model)
{
// todo: return properties
}
Then you do this in the Initialize and in the Save method and you would have 2 sets of properties (pre state and post state). Now you have that, it's easy to calculate the differences:
public static Dictionary<string, object> GetChangedProperties(Dictionary<string, object> preState, Dictionary<string, object> postState)
{
// todo: calculate difference
}
Now you have the difference, you can create a memento batch and it would restore the exact state as you expected.
ps. it would be great if you could put this into a blog post once done or create a PR with this feature

Related

Child DAC are not inserting when add a new Item

I have 2 scenarios, I am facing issue.
I have 3 DAC, One is Parent and other 2 are child DAC.
I have tab view, and showing data accordingly.
First tab showing Parent DAC data.
Second and third are showing child DAC data.
I am creating new item, Now i press Add(+) button and fill, Information in Parent tab, but not filled any data in 2nd and 3rd tab, when i press SAVE then only parent DAC saved, 2nd and 3rd tab rows not created in database, I want to save 2nd and 3rd Tab DAC also, what should i do?
Another case:
i am not allowing DAC to update, I am setting DAC.Cahce.AllowUpdated = false; all controls are disabled excepting Checkbox, why?
Also when we are on 3rd tab and click on toolbar Navigation button, then 1st tab data doesn't refresh, it is showing previous selected item data. How to fix this issue?
We are not using PXGrid for any child data, there is one to one relation ship between all 3 DAC.
/// Parent View
public SelectFrom<Parent>.View parentTran;
/// Child view
public SelectFrom<Child> .Where<Child.tranId.IsEqual<Parent.nCTranId.AsOptional>>.View
childAnalysis;
#region TranId
// Child DAC Field.
public abstract class tranId:PX.Data.BQL.BqlInt.Field<tranId> {
}
[PXDBInt(IsKey = true)]
[PXDBDefault(typeof(Parent.nCTranId))]
[PXParent(typeof(Select<Parent, Where<Parent.nCTranId, Equal<Current<tranId>>>>))]
public virtual int? TranId {
get; set;
}
#endregion
Take a look at T210 regarding Master-Detail Relationships. It contains a section on inserting a default record, which sounds like what you are trying to do with your child DAC. The training can be found at https://openuni.acumatica.com/courses/development/t210-development-customized-forms-and-master-detail-relationship/ on the Acumatica website. Specifically look at pages 103 and 104.
In step 3 of the section, you will see use of the RowInserted event to insert the child record. Since the RowInserted event fires only when creating a record, making it safe to insert the child record.
See the following code sample. I tried to use as much of your sample as possible, but I merged it with the technique shown in the training guide. Disclaimer: It will compile, but I have not built the prerequisites to fully test the code.
using PX.Data;
using PX.Data.BQL.Fluent;
namespace SSCS.CodeSample
{
public class CodeSampleEntry : PXGraph<CodeSampleEntry, DACParent>
{
#region Data Views
/// Parent View
[PXViewName("Parent View - Primary")]
public SelectFrom<DACParent>.View ParentTran;
/// Child view
[PXViewName("Child View")]
public SelectFrom<DACChild>
.Where<DACChild.tranId.IsEqual<DACParent.nCTranId.FromCurrent>>
.View ChildAnalysis;
#endregion
// RowInserted Event to Create a Default Child Record
// See T210 Master-Detail training guide from Acumatica
protected virtual void _(Events.RowInserted<DACParent> e)
{
// Ensure there isn't a child record already
if (ChildAnalysis.Select().Count == 0)
{
// Save state of IsDirty so that we can restore it
// after inserting the default child record
// so that we don't force the user to save
// unless they actually enter data somewhere
bool oldDirty = ChildAnalysis.Cache.IsDirty;
// Create an object for the child
// and fill in any data that should be
// populated by default
DACChild newChild = new DACChild();
newChild.MyData = "My Value";
// Insert the new child record into the cache
ChildAnalysis.Insert(newChild);
// Restore the IsDirty flag
// The insert above will have marked the view as
// dirty. If it wasn't dirty before, it should be
// treated as if it still isn't
ChildAnalysis.Cache.IsDirty = oldDirty;
}
}
}
[PXCacheName("Parent DAC")]
public class DACParent : IBqlTable
{
#region NCTranId
[PXDBIdentity]
public virtual int? NCTranId { get; set; }
public abstract class nCTranId
: PX.Data.BQL.BqlInt.Field<nCTranId> { }
#endregion
}
[PXCacheName("Child DAC")]
public class DACChild : IBqlTable
{
#region TranId
[PXDBInt(IsKey = true)]
[PXDBDefault(typeof(DACParent.nCTranId))]
[PXParent(typeof(SelectFrom<DACParent>
.Where<DACParent.nCTranId.IsEqual<DACChild.tranId.FromCurrent>>))]
public virtual int? TranId { get; set; }
public abstract class tranId
: PX.Data.BQL.BqlInt.Field<tranId> { }
#endregion
#region MyData
[PXDBString()]
[PXUIField(DisplayName = "My Data Field")]
public virtual string MyData { get; set; }
public abstract class myData
: PX.Data.BQL.BqlString.Field<myData> { }
#endregion
}
}

How can I execute code from the Release / Release All buttons in the Release AR Documents screen

I've got a customization to the Invoice & Memo screen where I execute some custom code (web service calls) when the Release action is activated. This works fine - I knew how to replace the PXAction code and proceeded from there. Now I want to use the Release AR Documents processing screen to do the same thing, but I'm having trouble understanding where / what to override, or where to place my code.
I see the ARDocumentRelease graph constructor with the SetProcessDelegate in the source code, but I'm not sure how to proceed - whether this is where I need to be looking or not. I need to execute my code for each line being released, using the RefNbr in my code.
Since it's an static method, you can't override it. Also, you can't do like it's done in the T300, because you are in processing graph and you can't override the release button with your own. I was able to achieve it by passing callback for each AR document that have been processed.
You can call the Initialize method of the ARDocumentRelease graph to override the logic like you said. After you just have to call ReleaseDoc that uses a callback parameter instead of using the default one.
Here's the code that I came with:
public class ARDocumentRelease_Extension : PXGraphExtension<ARDocumentRelease>
{
public override void Initialize()
{
ARSetup setup = Base.arsetup.Current;
Base.ARDocumentList.SetProcessDelegate(
delegate (List<BalancedARDocument> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (BalancedARDocument doc in list)
{
newlist.Add(doc);
}
AddAdditionalLogicToRelease(newlist);
}
);
Base.ARDocumentList.SetProcessCaption("Release");
Base.ARDocumentList.SetProcessAllCaption("Release All");
}
public delegate void PostPorcessing(ARRegister ardoc, bool isAborted);
private void AddAdditionalLogicToRelease(List<ARRegister> newlist)
{
ARDocumentRelease.ReleaseDoc(newlist, true, null, delegate(ARRegister ardoc, bool isAborted) {
//Add your logic to handle each document
//Test to check if it was not aborted
});
}
}
Please note that you must always call static methods from within long running process and create necessary objects there.
Processing delegate logic is implemented as long running process which creates worker thread to execute the processing logic.
You have AddAdditionalLogicToRelease() method which requires object instance in order to call and will fail during thread context switches and hence the issue. So, you must have create object instance inside the thread context and then call instance method.
In general, method that gets called from long running processes are declared static and required objects/graphs are created inside this static method to do some work. See below example how to properly override ARDocumentRelease graph for this purpose:
public class ARDocumentRelease_Extension : PXGraphExtension<ARDocumentRelease>
{
public override void Initialize()
{
Base.ARDocumentList.SetProcessDelegate(
delegate (List<BalancedARDocument> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (BalancedARDocument doc in list)
{
newlist.Add(doc);
}
// use override that allows to specify onsuccess routine
ARDocumentRelease.ReleaseDoc(newlist, true, null, (ardoc, isAborted) =>
{
//Custom code here, such as create your GL
});
}
);
}
}
I think it's the function
public static void ReleaseDoc(List<ARRegister> list, bool isMassProcess, List<Batch> externalPostList, ARMassProcessDelegate onsuccess)
under ARDocumentRelease businesss logic.

Binding a ReactiveCommand prevents a ViewModel from being garbage collected

When I bind a "back button" to a the router in ReactiveUI, my ViewModel is no longer garbage collected (my view too). Is this a bug, or is this me doing something dumb?
Here is my MeetingPageViewModel:
public class MeetingPageViewModel : ReactiveObject, IRoutableViewModel
{
public MeetingPageViewModel(IScreen hs, IMeetingRef mRef)
{
HostScreen = hs;
}
public IScreen HostScreen { get; private set; }
public string UrlPathSegment
{
get { return "/meeting"; }
}
}
Here is my MeetingPage.xaml.cs file:
public sealed partial class MeetingPage : Page, IViewFor<MeetingPageViewModel>
{
public MeetingPage()
{
this.InitializeComponent();
// ** Comment this out and both the View and VM will get garbage collected.
this.BindCommand(ViewModel, x => x.HostScreen.Router.NavigateBack, y => y.backButton);
// Test that goes back right away to make sure the Execute
// wasn't what was causing the problem.
this.Loaded += (s, a) => ViewModel.HostScreen.Router.NavigateBack.Execute(null);
}
public MeetingPageViewModel ViewModel
{
get { return (MeetingPageViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MeetingPageViewModel), typeof(MeetingPage), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (MeetingPageViewModel)value; }
}
}
I then run, and to see what is up, I use VS 2013 Pro, and turn on the memory analyzer. I also (as a test) put in forced GC collection of all generations and a wait for finalizers. When that line is uncommented above, when all is done, there are three instances of MeetingPage and MeetingPageViewModel. If I remove the BindCommand line, there are no instances.
I was under the impression that these would go away on their own. Is the problem the HostScreen object or the Router that refers to an object that lives longer than this VM? And that pins things down?
If so, what is the recommended away of hooking up the back button? Using Splat and DI? Many thanks!
Following up on the idea I had at the end, I can solve this in the following way. In my App.xaml.cs, I make sure to declare the RoutingState to the dependency injector:
var r = new RoutingState();
Locator.CurrentMutable.RegisterConstant(r, typeof(RoutingState));
then, in the ctor of each view (the .xaml.cs code) with a back button for my Windows Store app, I no longer use the code above, but replace it with:
var router = Locator.Current.GetService<RoutingState>();
backButton.Click += (s, args) => router.NavigateBack.Execute(null);
After doing that I can visit the page as many times as I want and never do I see the instances remaining in the analyzer.
I'll wait to mark this as an answer to give real experts some time to suggest another (better?) approach.

Listbox not always adding item using Windows Azure MobileServiceCollection with WP8

I'm using Windows Azure Mobile Services to store and retrieve data in my Windows Phone 8 app. This is a bit of a complicated issue so I will do my best to explain it.
Firstly I'm using raw push notifications to receive a message and when it receives the message it updates a listbox in my app. When I open my app, navigate to the page with the ListBox and receive a push notification the ListBox updates fine. If I press back, then navigate to the same page with the ListBox, the push notification is received, the code to update the ListBox executes with no errors yet the ListBox doesn't update. I have checked that the same code runs using the OnNavigatedTo handler in both scenarios, but it seems like the ListBox does not bind correctly in the second instance when I press back and then re-navigate to the same page. Here are some code snippets:
MobileServiceCollection declarations:
public class TodoItem
{
public int Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
}
private MobileServiceCollection<ToDoItem, ToDoItem> TodoItems;
private IMobileServiceTable<TodoItem> todoTable = App.MobileService.GetTable<TodoItem>();
Push Notification Received Handler:
void PushChannel_HttpNotificationReceived(object sender, HttpNotificationEventArgs e)
{
string message;
using (System.IO.StreamReader reader = new System.IO.StreamReader(e.Notification.Body))
{
message = reader.ReadToEnd();
}
Dispatcher.BeginInvoke(() =>
{
var todoItem = new TodoItem
{
Text = message,
};
ToDoItems.Add(todoItem);
}
);
}
I have tried using:
ListItems.UpdateLayout();
and
ListItems.ItemsSource = null;
ListItems.ItemsSource = ToDoItems;
before and after the code in the above procedure that adds the ToDoItem but it didn't help.
The following procedure is called in my OnNavigatedTo event handler, and refreshes the Listbox and assigns ToDoItems as the items source:
private async void RefreshTodoItems()
{
try
{
ToDoItems = await todoTable
.ToCollectionAsync();
}
catch (MobileServiceInvalidOperationException e)
{
MessageBox.Show(e.Message, "Error loading items", MessageBoxButton.OK);
}
ListItems.ItemsSource = ToDoItems;
}
The above procedure is async but I have made sure it completes before receiving any notifications. Even so, as mentioned above when I open the app, navigate to the page that shows the ListBox it updates fine. When I press back, navigate to the same page again, it doesn't work. When I back out of the app, re-open it, navigate to the page with the ListBox, it works again, and then fails if I press back and re-open the page. So it seems the ListBox is not binding to ToDoItems correctly when I press back and navigate to the same page.
Any help appreciated. Thanks.
Can you modify your approach a bit to use Data Binding and the MVVM model to bind your model to your view.
It might look like a bit of effort initially but will save you a lot of debugging hours later on.
Just follow the below steps
Create a new class that implements INotifyPropertyChanged
Add the below method implementation
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Add public ObservableCollection<TodoItem> TodoItems{ get; private set; } and initialize it in the constructor.
Every PhoneApplicationPage has a DataContext member. Assing it to a singleton instance of the above class that you create.
In the XAML, add the property ItemsSource="{Binding TodoItems}" to the list.
In the DataTemplate of the list use ItemsSource="{Binding Text}" for the control you wish to display this value on. ( e.g. TextBlock )
Now whenever you add elements to the collection, it will be reflected in the UI, and vice-versa.

Create Orchard ContentItem programmatically from Command

I've written the following simple command inside my module. The Faq type has one custom part with a single field, and one BodyPart. After _cm.Create(item) is run, the item has an Id assigned but I can't find any trace of it in the database and it doesn't appear in Orchard's content tab. Why does the item get an Id but isn't found in the database? And does it need a driver, view, and placement info before it appears in the content tab?
public class ApiCommands : DefaultOrchardCommandHandler
{
private readonly IContentManager _cm;
public ApiCommands(IContentManager cm)
{
_cm = cm;
}
[CommandName("api seed")]
public void Seed()
{
var item = _cm.New("Faq");
item.As<FaqPart>().Question = "Why is the sky blue?";
item.As<BodyPart>().Text = "Shut up and do your homework.";
_cm.Create(item);
}
}
My custom part has no driver this is the Handler:
public FaqHandler(IRepository<FaqPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
It turns out my type didn't attach a CommonPart. After I attached one and set the Owner property of the part, I was able to save it.

Resources