.net maui MVVM Binding a SelectedItemCommand and SelectedItemParameter from a CollectionView - collectionview

So I am working with SQLite, CommunityToolkit.Mvvm.ComponentModel;
I have database containing a table of friends. I can bind this to a CollectionView.
I am following https://www.youtube.com/watch?v=8_cqUvriwM8 but trying to use MVVM approach.
I can get it to work happily with SelectionChanged and an event, but not with SelectionChangedCommand and I can't get access to the Friend item in the list.
Here is the relevant xaml
<CollectionView Grid.Row="2"
x:Name="FriendsList"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="{Binding .}"
SelectionChanged="OnSelectionChanged" >
Here is the relevant part of the code (I'm using the code behind for the xaml just for testing)
public MainPage()
{
InitializeComponent();
this.BindingContext = this; //cool for binding the xaml to the code behind.
}
...
//This works fine (so why do I bother with mvvm?)
public void OnSelectionChanged(Object sender, SelectionChangedEventArgs e)
{
Console.WriteLine("Selection changed click");
Friend f = e.CurrentSelection[0] as Friend;
Console.WriteLine(f.LName);
}
//Can't get this to work, though it will register the click
public ICommand SelectionChangedCommand => new Command(SelectionChangedControl);
public void SelectionChangedControl()
{
Console.WriteLine("selection made");
}
My thinking was that if I could do this to get at the Friend item since the CommandParameter is, as I understand, to provide an object?
public ICommand SelectionChangedCommand => new Command<Friend>(SelectionChangedControl);
public void SelectionChangedControl(Friend f)
{
Console.WriteLine("selection made");
}
But the command doesn't even fire now. Clearly I am way off beam.
Any ideas please. (Oh by the way I have tried commenting out one or the other just in case).
BTW is there a reference (not MS docs) which explains this stuff in beginners terms?
Is there an API reference to dot net Maui?
EDIT: From the documentation https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/collectionview/selection
Single selection
When the SelectionMode property is set to Single, a single item in the CollectionView can be selected. When an item is selected, the SelectedItem property will be set to the value of the selected item. When this property changes, the SelectionChangedCommand is executed (with the value of the SelectionChangedCommandParameter being passed to the ICommand, and the SelectionChanged event fires.
How do I get at value of the SelectionChangedCommandParameter, i.e. the row object, i.e. my Friend object?
EDIT2: Somehow I think I need to get at the CurrentSelection[0] but I don't know how.
I've learnt that I can do something like this (from the docs)
SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="Hello G"
and
public ICommand SelectionChangedCommand => new Command<string>( (String s) =>
{
Console.WriteLine($"selection made {s}");
});
and the command is picked up and displayed, so my thinking is that using {Binding .} is not what I want, but what do I bind to?
SelectionChangedCommandParameter ={Binding ???}
Thanks, G.

first, bind SelectedItem
SelectedItem="{Binding SelectedFriend}"
then in your VM create a property for that bound item
public Friend SelectedFriend { get; set; }
then in your Command you can use that property
public void SelectionChangedControl()
{
Console.WriteLine(SelectedFriend.Name);
}

When you use . at CollectionView.SelectionChangedCommandParameter, it points at the BidingContext of its parent view.
e.g. If your CollectionView is in a ContentPage, . points at the BindingContext of the ContentPage.
If you want a reference of each item in FriendsList, one of solutions is use SelectedItem.
Try something like this:
<CollectionView
Grid.Row="2"
x:Name="FriendsList"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="{Binding Path=SelectedItem, Source={x:Reference FriendsList}}">
or
<CollectionView
Grid.Row="2"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="{Binding Path=SelectedItem, Source={RelativeSource Self}}">
References:
Bind to self (Source={RelativeSource Self}}):
https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/relative-bindings#bind-to-self

Note for Multiple Selections
I got hung up trying to bind multiple selections to the view model without linking it in the code behind. This page was the only relevant search result and helped a lot, but was missing a piece for multiple selections.
View.xaml
<CollectionView ItemsSource="{Binding DataItems}"
SelectedItems="{Binding SelectedData}"
SelectionMode="Multiple"
SelectionChangedCommand="{Binding SelectionChangedCommand}">
....
Couple of things to mention for the view model. I'm using CommunityToolkit.Mvvm, so the [ObservableProperty] annotation creates the property for you in proper camel case, and the [RelayCommand] for OnMethodName will drop the 'On' and just be MethodNameCommand.
ViewModel.cs
[ObservableProperty]
ObservableCollection<CustomDataItem> dataItems;
[ObservableProperty]
ObservableCollection<object> selectedData;
[RelayCommand]
void OnSelectionChanged()
{
foreach(var o in SelectedData)
{
if(o is CustomDataItem i)
...
}
}
The major takeaway though is that the SelectedItems must be a List<object> , they cannot be the <CustomDataItem>. I spent a couple hours searching and trying different things until I gave up and just linked the event handler in the code behind. But then I couldn't pre-select the items as described here until I changed them to the object list. So that list will populate both ways and you just have to cast it to the data type you're using.
Anyway, might've been obvious for some but maybe this will help anyone like me who just assumed the SelectedItems would be the same as the SelectedItem but in a list.

#Jason I'm laughing so much, I just figured it out and then came to post and saw your answer. Thankyou so much for your help.
For the record I found this post https://www.mfractor.com/blogs/news/migrating-listview-to-collectionview-in-xamarin-forms-interactivity
and eventually I figured out that I needed the SelectedItem as you pointed out. I think that because this wasn't needed (or is implicit) in the SelectionChanged click event.
Anyhow in my xaml
<CollectionView Grid.Row="2"
x:Name="FriendsList"
SelectionMode="Single"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="{Binding .}" >
In my code
public Friend SelectedItem { get; set; }
//respond to item select
public ICommand SelectionChangedCommand => new Command<Object>((Object e) =>
{
Console.WriteLine($"selection made {SelectedItem.FName}");
});
Your code is much simpler of course.
You pointed out that SelectionChangedCommandParameter="{Binding .}" was (probably) not needed, so what is it's purpose?
What is the object e that is being returned in my code? I assume it is related to the SelectionChangedCommandParameter?
In my immediate window I get
e
{Census.MainPage}
base: {Microsoft.Maui.Controls.ContentPage}
AddFriendCommand: {Microsoft.Maui.Controls.Command}
SelectedItem: {Census.Classes.Friend}
SelectionChangedCommand: {Microsoft.Maui.Controls.Command<object>}
And is it possible to trace through from the xaml to the code. For instance when I was trying to figure things out I would have liked to have trapped the item click event in the xaml and see what is was doing? (Especially since it didn't at times touch a breakpoint in my code.
Just idle questions and not expecting or needing an answer unless someone is so inclined.
Thank you much again #Jason, you are a star! :)

Related

Make Two Views Share Click Feedback in Constraint Layout

I have this situation where I have a constraint layout. Within it lies two views. An ImageView and a TextView. When either of these Views is clicked, I want both to produce a feedback (text color change for textview and drawable tint in imageview) but I can't seem to think of a way to do these unless I put them inside another viewgroup.
Can someone show me how this could be done in constraint Layout? thank you.
Take a look at performClick().
performClick
boolean performClick ()
Call this view's OnClickListener, if it is defined. Performs all normal actions associated with clicking: reporting accessibility event, playing a sound, etc.
The idea is that when one view is clicked, your code will call performClick() on the other view. You will have to make sure that you inhibit any duplication of actions if the two views do the same function.
Other than doing this in code, I don't know of a way using just XML. There is the concept of a Group in ConstraintLayout but that just a way to control the visibility of the members of the group and does not extend to other properties.
I would use another enclosing view group unless you have a requirement not to. I just seems easier.
Use Group concept in ConstraintLayout refer: https://developer.android.com/reference/android/support/constraint/Group ,https://riggaroo.co.za/constraintlayout-guidelines-barriers-chains-groups/ ,
in java
Group group = findViewById(R.id.group);
int refIds[] = group.getReferencedIds();
for (int id : refIds) {
findViewById(id).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// your code here.
}
});
}
Kotlin:
fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
Then call the function on the group:
group.setAllOnClickListener(View.OnClickListener {
// your code here.
})

Data View Customization in Extension

I have overwritten the data view for a custom graph in an extension, which returns the correct data without issue, both by re-declaring the view, and using the delegate object techniques. The issue is that when I do, the AllowSelect/AllowDelete modifications on the view in the primary graph stop working, once I comment out the overwrite, the logic works as normal.
Not sure what I'm missing, but any thoughts would be appreciated
Edit: To clarify, on the main graph, without the extension, the data retrieval and Allow... work without issue
public class FTTicketEntry : PXGraph<FTTicketEntry, UsrFTHeader>
{
public PXSelect<UsrFTHeader> FTHeader;
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>>> FTGridLabor;
And with the extension, the data is returned correctly from the modified view, but the Allow... do not work from the main graph, only when entered on the extension
public class FTTicketEntryExtension : PXGraphExtension<FTTicketEntry>
{
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>, And<UsrFTGridLabor.projectID, Equal<Current<UsrFTHeader.projectID>>, And<UsrFTGridLabor.taskID, Equal<Current<UsrFTHeader.taskID>>>>>> FTGridLabor;
I have also tried the other process on the extension with the same results, the data is filtered correctly, but the Allow... commands fail.
public PXSelect<UsrFTGridLabor, Where<UsrFTGridLabor.ticketNbr, Equal<Current<UsrFTHeader.ticketNbr>>>> FTGridLabor;
public virtual IEnumerable fTGridLabor()
{
foreach (PXResult<UsrFTGridLabor> record in Base.FTGridLabor.Select())
{
UsrFTGridLabor p = (UsrFTGridLabor)record;
if (p.ProjectID == Base.FTHeader.Current.ProjectID && p.TaskID == Base.FTHeader.Current.TaskID)
{
yield return record;
}
}
}
My main concern with not wanting to use PXSelectReadOnly, is that there is a status field on the header which drives when certain combinations of the conditions are required and are called on the rowselected events, sometimes all and sometimes none, and the main issue is that I obviously don't want to have to replicate all of the UI logic into the extension, when overwriting the view was the main intent of the extension for the screen.
Appreciate the assistance, and hopefully you see something I'm overlooking or have missed
Thanks
Every BLC instance stores all actual data views and actions within 2 collections: Views and Actions. Whenever, you customize a data view or an action with a BLC extension, the original data view / action gets replaced in the appropriate collection by your custom object declared within the extension class. After the original data view or action was removed from the appropriate collection, it's quite obvious that any change made to the original object will not make any effect, since the original object is not used by the BLC anymore.
The easiest way to access actual object from either of these 2 collections would be as follows: Views["FTGridLabor"].Allow... = value;
Alternatively, you might operate with AllowInsert, AllowUpdate and AllowDelete properties on the cache level: FTGridLabor.Cache.Allow... = value;
By changing AllowXXX properties on the cache level, you completely eliminate the need for setting AllowXXX on the data view, since PXCache.AllowXXX properties have higher priority when compared to identical properties on the data view level:
public class PXView
{
...
protected bool _AllowUpdate = true;
public bool AllowUpdate
{
get
{
if (_AllowUpdate && !IsReadOnly)
{
return Cache.AllowUpdate;
}
return false;
}
set
{
_AllowUpdate = value;
}
}
...
}
With all that said, to resolve your issue with UI Logic not applying to modified view, please consider one of the following options:
Set AllowXXX property values in both the original BLC and its extensions via the object obtained from the Views collection:
Views["FTGridLabor"].Allow... = value;
operate with AllowXXX property values on the cache level: FTGridLabor.Cache.Allow... = value;
First check if your DataView should/should not be a variant of PXSelectReadonly.
Without more information my advice would be to set the Allow properties in Initialize method of your extension:
public override void Initialize()
{
// This is similar to PXSelectReadonly
DataView.AllowDelete = false;
DataView.AllowInsert = false;
DataView.AllowUpdate = false;
}

Using a Textfield to switch to a displayable in Java Me

I'm working with java me, I built an app using forms displayables. I'm trying to switch to other forms, based on the user's input in a textfield item. For example, I want the user to be able to type in the number "1" in the textfield and then be taken to form1 or type in "2" and be taken to form2 etc.
What's the code to do this?
Here's what I did but it's not working as expected:
form.setItemStateListener(new ItemStateListener() {
public void itemStateChanged(Item item) {
if (item == TextField) {
if ("1".equals(TextField.getString())) {
switchDisplayable(null, form1);
}
}
}
I've done as adviced. I added a command to the textfield item and listen on it to read textfield contents and then compare the contents as a string, to switch forms. See my code below, still not working. I think maybe there's something I'm missing or my logic is not right.
form.setCommandListener(new CommandListener() {
public void commandAction(Command command, Displayable displayable) {
if (command == getTextFieldItemCommand()) {
if ("1".equals(TextField.getString())) {
switchDisplayable(null, form1);
} else if ("2".equals(TextField.getString())){
switchDisplayable(null, form2);
}
}
}
It looks like you expect method itemStateChanged to be invoked when it feels convenient to you, like at every character entry in the text field.
Above expectation is wrong, specified behavior is explained in API javadocs:
It is up to the device to decide when it considers a new value to have been entered into an Item... In general, it is not expected that the listener will be called after every change is made...
Given above, using itemStateChanged the way you want makes very little sense, consider changing design of your MIDlet.
I for one would probably just add a command Go and command listener to the form or text field and read text field contents when user invokes that command to find out which displayable they want to switch to.

Orchard CMS: Do I have to add a new layer for each page when the specific content for each page is spread in different columns?

Lets say I want a different main image for each page, situated above the page title. Also, I need to place page specific images in the left bar, and page specific text in the right bar. In the right and left bars, I also want layer specific content.
I can't see how I can achieve this without creating a layer for each and every page in the site, but then I end up with a glut of layers that only serve one page which seems too complex.
What am I missing?
If there is a way of doing this using Content parts, it would be great if you can point me at tutorials, blogs, videos to help get my head round the issue.
NOTE:
Sitefinity does this sort of thing well, but I find Orchard much simpler for creating module, as well as the fact that it is MVC which I find much easier.
Orchard is free, I understand (and appreciate) that. Just hoping that as the product evolves this kind of thing will be easier?
In other words, I'm hoping for the best of all worlds...
There is a feature in the works for 1.5 to make that easier, but in the meantime, you can already get this to work quite easily with just a little bit of code. You should first add the fields that you need to your content type. Then, you are going to send them to top-level layout zones using placement. Out of the box, placement only targets local content zones, but this is what we can work around with a bit of code by Pete Hurst, a.k.a. randompete. Here's the code:
ZoneProxyBehavior.cs:
=====================
using System;
using System.Collections.Generic;
using System.Linq;
using ClaySharp;
using ClaySharp.Behaviors;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneProxyBehavior : ClayBehavior {
public IDictionary<string, Func<dynamic>> Proxies { get; set; }
public ZoneProxyBehavior(IDictionary<string, Func<dynamic>> proxies) {
Proxies = proxies;
}
public override object GetMember(Func<object> proceed, object self, string name) {
if (name == "Zones") {
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new ZonesProxyBehavior(()=>proceed(), Proxies, self)
});
}
// Otherwise proceed to other behaviours, including the original ZoneHoldingBehavior
return proceed();
}
public class ZonesProxyBehavior : ClayBehavior {
private readonly Func<dynamic> _zonesActivator;
private readonly IDictionary<string, Func<dynamic>> _proxies;
private object _parent;
public ZonesProxyBehavior(Func<dynamic> zonesActivator, IDictionary<string, Func<dynamic>> proxies, object self) {
_zonesActivator = zonesActivator;
_proxies = proxies;
_parent = self;
}
public override object GetIndex(Func<object> proceed, object self, IEnumerable<object> keys) {
var keyList = keys.ToList();
var count = keyList.Count();
if (count == 1) {
// Here's the new bit
var key = System.Convert.ToString(keyList.Single());
// Check for the proxy symbol
if (key.Contains("#")) {
// Find the proxy!
var split = key.Split('#');
// Access the proxy shape
return _proxies[split[0]]()
// Find the right zone on it
.Zones[split[1]];
}
// Otherwise, defer to the ZonesBehavior activator, which we made available
// This will always return a ZoneOnDemandBehavior for the local shape
return _zonesActivator()[key];
}
return proceed();
}
public override object GetMember(Func<object> proceed, object self, string name) {
// This is rarely called (shape.Zones.ZoneName - normally you'd just use shape.ZoneName)
// But we can handle it easily also by deference to the ZonesBehavior activator
return _zonesActivator()[name];
}
}
}
}
And:
ZoneShapes.cs:
==============
using System;
using System.Collections.Generic;
using Orchard.DisplayManagement.Descriptors;
using Orchard;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneShapes : IShapeTableProvider {
private readonly IWorkContextAccessor _workContextAccessor;
public ZoneShapes(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
}
public void Discover(ShapeTableBuilder builder) {
builder.Describe("Content")
.OnCreating(creating => creating.Behaviors.Add(
new ZoneProxyBehavior(
new Dictionary<string, Func<dynamic>> { { "Layout", () => _workContextAccessor.GetContext().Layout } })));
}
}
}
With this, you will be able to address top-level layout zones using Layout# in front of the zone name you want to address, for example Layout#BeforeContent:1.
ADDENDUM:
I have used Bertrand Le Roy's code (make that Pete Hurst's code) and created a module with it, then added 3 content parts that are all copies of the bodypart in Core/Common.
In the same module I have created a ContentType and added my three custom ContentParts to it, plus autoroute and bodypart and tags, etc, everything to make it just like the Orchard Pages ContentType, only with more Parts, each with their own shape.
I have called my ContentType a View.
So you can now create pages for your site using Views. You then use the ZoneProxy to shunt the custom ContentPart shapes (Parts_MainImage, Parts_RightContent, Parts_LeftContent) into whatever Zones I need them in. And job done.
Not quite Sitefinity, but as Bill would say, Good enough.
The reason you have to create your own ContentParts that copy BodyPart instead of just using a TextField, is that all TextFields have the same Shape, so if you use ZoneProxy to place them, they all end up in the same Zone. Ie, you build the custom ContentParts JUST so that you get the Shapes. Cos it is the shapes that you place with the ZoneProxy code.
Once I have tested this, I will upload it as a module onto the Orchard Gallery. It will be called Wingspan.Views.
I am away on holiday until 12th June 2012, so don't expect it before the end of the month.
But essentially, with Pete Hurst's code, that is how I have solved my problem.
EDIT:
I could have got the same results by just creating the three content parts (LeftContent, RightContent, MainImage, etc), or whatever content parts are needed, and then adding them to the Page content type.
That way, you only add what is needed.
However, there is some advantage in having a standard ContentType that can be just used out of the box.
Using placement (Placement.info file) you could use the MainImage content part for a footer, for example. Ie, the names should probably be part 1, part 2, etc.
None of this would be necessary if there was a way of giving the shape produced by the TextField a custom name. That way, you could add as may TextFields as you liked, and then place them using the ZoneProxy code. I'm not sure if this would be possible.

Subsonic - Where do i include my busines logic or custom validation

Im using subsonic 2.2
I tried asking this question another way but didnt get the answer i was looking for.
Basically i ususally include validation at page level or in my code behind for my user controls or aspx pages. However i haev seen some small bits of info advising this can be done within partial classes generated from subsonic.
So my question is, where do i put these, are there particular events i add my validation / business logic into such as inserting, or updating. - If so, and validation isnt met, how do i stop the insert or update. And if anyone has a code example of how this looks it would be great to start me off.
Any info greatly appreciated.
First you should create a partial class for you DAL object you want to use.
In my project I have a folder Generated where the generated classes live in and I have another folder Extended.
Let's say you have a Subsonic generated class Product. Create a new file Product.cs in your Extended (or whatever) folder an create a partial class Product and ensure that the namespace matches the subsonic generated classes namespace.
namespace Your.Namespace.DAL
{
public partial class Product
{
}
}
Now you have the ability to extend the product class. The interesting part ist that subsonic offers some methods to override.
namespace Your.Namespace.DAL
{
public partial class Product
{
public override bool Validate()
{
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.Errors.Add("ProductName cannot be empty");
return Errors.Count == 0;
}
// another way
protected override void BeforeValidate()
{
if (string.IsNullOrEmpty(this.ProductName))
throw new Exception("ProductName cannot be empty");
}
protected override void BeforeInsert()
{
this.ProductUUID = Guid.NewGuid().ToString();
}
protected override void BeforeUpdate()
{
this.Total = this.Net + this.Tax;
}
protected override void AfterCommit()
{
DB.Update<ProductSales>()
.Set(ProductSales.ProductName).EqualTo(this.ProductName)
.Where(ProductSales.ProductId).IsEqualTo(this.ProductId)
.Execute();
}
}
}
In response to Dan's question:
First, have a look here: http://github.com/subsonic/SubSonic-2.0/blob/master/SubSonic/ActiveRecord/ActiveRecord.cs
In this file lives the whole logic I showed in my other post.
Validate: Is called during Save(), if Validate() returns false an exception is thrown.
Get's only called if the Property ValidateWhenSaving (which is a constant so you have to recompile SubSonic to change it) is true (default)
BeforeValidate: Is called during Save() when ValidateWhenSaving is true. Does nothing by default
BeforeInsert: Is called during Save() if the record is new. Does nothing by default.
BeforeUpdate: Is called during Save() if the record is new. Does nothing by default.
AfterCommit: Is called after sucessfully inserting/updating a record. Does nothing by default.
In my Validate() example, I first let the default ValidatColumnSettings() method run, which will add errors like "Maximum String lenght exceeded for column ProductName" if product name is longer than the value defined in the database. Then I add another errorstring if ProductName is empty and return false if the overall error count is bigger than zero.
This will throw an exception during Save() so you can't store the record in the DB.
I would suggest you call Validate() yourself and if it returns false you display the elements of this.Errors at the bottom of the page (the easy way) or (more elegant) you create a Dictionary<string, string> where the key is the columnname and the value is the reason.
private Dictionary<string, string> CustomErrors = new Dictionary<string, string>
protected override bool Validate()
{
this.CustomErrors.Clear();
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.CustomErrors.Add(this.Columns.ProductName, "cannot be empty");
if (this.UnitPrice < 0)
this.CustomErrors.Add(this.Columns.UnitPrice, "has to be 0 or bigger");
return this.CustomErrors.Count == 0 && Errors.Count == 0;
}
Then if Validate() returns false you can add the reason directly besides/below the right field in your webpage.
If Validate() returns true you can safely call Save() but keep in mind that Save() could throw other errors during persistance like "Dublicate Key ...";
Thanks for the response, but can you confirm this for me as im alittle confused, if your validating the column (ProductName) value within validate() or the beforevalidate() is string empty or NULL, doesnt this mean that the insert / update has already been actioned, as otherwise it wouldnt know that youve tried to insert or update a null value from the UI / aspx fields within the page to the column??
Also, within asp.net insert or updating events we use e.cancel = true to stop the insert update, if beforevalidate failes does it automatically stop the action to insert or update?
If this is the case, isnt it eaiser to add page level validation to stop the insert or update being fired in the first place.
I guess im alittle confused at the lifecyle for these methods and when they come into play

Resources