[Extreme newbie] Adding contributed actions

Nov 10, 2012 at 4:25 PM
Edited Nov 10, 2012 at 4:27 PM

Hi,

As the subject implies, I'm a newbie with NO, taking the first steps in my first NO-based project.

I'm trying to add a contributed action, for now just as a "proof of concept".  I'm afraid I can't get the action to appear in the UI.  The domain object I'm working with is Customer.

What I've done is:

  • Created a new contributed actions class:

 

[DisplayName("Customers")]
public class CustomerContributedActions : AbstractService
{
    public Customer Create2(Customer context)
    {
        var customer = NewTransientInstance<Customer>();
        return customer;
    }
}
  • Registered the class in RunWeb.cs:
protected override IServicesInstaller ContributedActions 
{ get { return new ServicesInstaller(new CustomerContributedActions()); } }

AFAIK, that's all I had to do.  I might have missed the point with the context parameter in the action method.  Other than that, I'm clueless.

 

Any ideas?

 

Coordinator
Nov 10, 2012 at 6:10 PM

Newbie questions are fine!  So far as I can see you have done everything correctly for a contributed action, which should appear contributed to a Customer object i.e. on the Actions menu for a Customer instance (under a sub-menu "Customers").

But I am wondering if you really wanted to write a contributed action at all.  Your action creates a Customer, which would be an unusual thing to do from an existing Customer object (though not impossible).  I wonder if what you wanted to write was a regular service action, perhaps  -  to create a new customer from scratch?

If so, your method (Create2) should not take any parameters, and your service  (CustomerContributedActions  -  though you might want to rename it if this is the case) should be registered not in the ContributedActions property of RunWeb, but in the MenuServices.  That way your action will appear under a "Customers' menu on the main menu bar at the top of the screen.

Nov 10, 2012 at 7:31 PM

Thanks Richard.

Regarding your later comment, you're obviously right.  I just wanted to experiment with contributed actions.  It should have been "Upgrade", "Clone" instead of "Create2".

The thing is, with the above code, I can't see any change from the default "Customer" sub-menu, created using the SimpleRepository service.  That's my main concern for now :)

Coordinator
Nov 10, 2012 at 8:29 PM

Thanks for the clarification.  But I think you're looking in the wrong place.  The new action "Create2" won't be on the main Customers menu  - it should be on the 'Actions' menu for an individual object.  To avoid possible confusion (of the framework, or the user) I recommend that you give your CustomerContributedActions a different name (using DisplayName, as you are) than "Customers"  -  since that name is being displayed by your SimpleRepository<Customer>. 

Coordinator
Nov 11, 2012 at 9:07 AM

Thinking on this further  -  I'm now realising that it is the naming that causing the problem i.e. you have two services with the same name "Customers"  - the SimpleRepository<Customer> and your CustomerContributedActions.   I'm think that there's a dictionary of services by name, so only one of them is being registered or something like that.  I'm surprised, though, that Naked Objects is not throwing an exception here  -  I will investigate further next week.

Meantime, either give them separate names, or meld your repository and contributed actions into a single service. Sorry I didn't realise earlier  -  I (wrongly) thought you had misunderstood the intent of contributed actions.

Nov 11, 2012 at 12:29 PM
Edited Nov 11, 2012 at 12:33 PM

Got that, but now I bumped into a few problems while trying to add a new customer.  Please let me know I should open a new thread for either of them:

  1. My customer entity contains an auto-incremented ID property, marked as "StoreGenreationPattern=Identity" in the model.  Nevertheless, I need to provide a value in the new customer form (the underlying property is not read-only).
  2. My customer entity contains computed columns, marked as "StoreGenerationPattern=computed" in the module.  Same problem as above.
  3. When I try to create a new object by providing values to the above properties, I get the following exception (not sure if it's related to the above): 

Error in: /Customer/Edit?id=Model.Customer%3B1%3BSystem.Int64%3B1%3BTrue%3B%3B0 error:
Server Error in '/' Application.
________________________________________
System.Data.Entity.DynamicProxies.Customer_DFEE87820E72A2575E838988BE0B19954DE101E124FE58A3FCC5691C84F0BA85 is not a IEntityWithChangeTracker (all properties must be made virtual/Overrideable and all collection properties should be of type ICollection)
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: NakedObjects.Core.Util.NakedObjectAssertException: System.Data.Entity.DynamicProxies.Customer_DFEE87820E72A2575E838988BE0B19954DE101E124FE58A3FCC5691C84F0BA85 is not a IEntityWithChangeTracker (all properties must be made virtual/Overrideable and all collection properties should be of type ICollection)

Source Error:

Line 40:         [HttpPost]
Line 41:         public override ActionResult Edit(ObjectAndControlData controlData, FormCollection form) {
Line 42:             return base.Edit(controlData, form);
Line 43:         }
Line 44:

p.s. I'm using VS2012/.NET4.5/EF5

Coordinator
Nov 11, 2012 at 3:47 PM

Several different issues here:

 

1.  For your Customer Id, you can just type in any integer for the Id  -  the database will just override it.  But much better to simply mark that Id property as [Hidden]  - then you don't see it and don't need to complete it.

2. The error message is unrelated to this, and is in fact self-explanatory:  'all properties must be made virtual/Overrideable and all collection properties should be of type ICollection)'.  This is arising because you have a property (or a collection) that is not marked virtual.  

Nov 11, 2012 at 4:02 PM

Thanks Richard.

As for the id (I assume it's the same for computed column properties), hiding it in the new customer form is all good, but I would like it to be visible in (for example) a detail/table view. Instead of "hidden", can it be marked as "read-only"?

As for the exception, it's indeed self-descriptive, but since the Customer entity is created by the Entity Framework, I'm a bit confused here.  Should I change the template I'm using or modify it?  When I followed the instructions in the "Creating a Model project" section of the developer manual, I got the impression that EF5's default POCO template generates the entities which can be interfaced with NO without any changes.  Is this really the case?  If so, what am I missing here?  I didn't extend the generated entities with any properties, and here's the Customer class generated by EF:

    public partial class Customer
    {
        public Customer()
        {
            this.ContentFolders = new HashSet<ContentFolder>();
            this.Sessions = new HashSet<Session>();
        }
   
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public string BandwidthHardLimit { get; set; }
        public string SessionHardLimit { get; set; }
        public bool IsSingleUserLimit { get; set; }
        public bool IsSingleIpAddressLimit { get; set; }
        public int CurrentBandwidth { get; set; }
        public int CurrentSessions { get; set; }
   
        public virtual ICollection<ContentFolder> ContentFolders { get; set; }
        public virtual ICollection<Session> Sessions { get; set; }
    }

Coordinator
Nov 11, 2012 at 4:19 PM

" Instead of "hidden", can it be marked as "read-only"?"

Mark it [Disabled] then.

"When I followed the instructions in the "Creating a Model project" section of the developer manual, I got the impression that EF5's default POCO template generates the entities which can be interfaced with NO without any changes."

Looks like I need to update that section of the manual, sorry.   I know that the default template used to create virtual properties, but perhaps that changed with the templates provided with EF5.  In any event, I recommend that  you use the Naked Objects > Default T 4 Template  -  as described in steps 4 & 5 of that section of the manual.

Nov 11, 2012 at 5:20 PM
  1. Do you mean I should add the [Disabled] attribute in the class generated by the template?  If I do so, the attribute will disappear after I update the model.
  2. I have changed the template to NO's template and now I'm not getting that exception anymore.  The bad news are that I'm getting a different exception :)

Error in: /Customer/Edit?id=Model.Customer%3B1%3BSystem.Int64%3B1%3BTrue%3B%3B0 error:
Server Error in '/' Application.
________________________________________
Object reference not set to an instance of an object.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Source Error:

Line 40:         [HttpPost]
Line 41:         public override ActionResult Edit(ObjectAndControlData controlData, FormCollection form) {
Line 42:             return base.Edit(controlData, form);
Line 43:         }
Line 44:

Source File: e:\Users\Ury\Projects\LiveNetIl\SMS\SmsWeb\Controllers\GenericController.cs    Line: 42

Stack Trace:

[NullReferenceException: Object reference not set to an instance of an object.]
   NakedObjects.EntityObjectStore.ObjectContextUtils.CreateObject(LocalContext context, Type type) +94
   NakedObjects.EntityObjectStore.EntityCreateObjectCommand.ProxyObject(Object originalObject, INakedObject adapterForOriginalObject) +106
   NakedObjects.EntityObjectStore.EntityCreateObjectCommand.ProxyObjectIfAppropriate(Object originalObject) +883
   NakedObjects.EntityObjectStore.EntityCreateObjectCommand.Execute(IExecutionContext executionContext) +220
   NakedObjects.EntityObjectStore.EntityObjectStore.ExecuteCommand(T command) +61
   NakedObjects.EntityObjectStore.EntityObjectStore.CreateCreateObjectCommand(INakedObject nakedObject) +148
   NakedObjects.Persistor.Objectstore.ObjectStorePersistor.AddPersistedObject(INakedObject nakedObject) +148
   NakedObjects.EntityObjectStore.EntityPersistAlgorithm.MakeObjectPersistent(INakedObject nakedObject, IPersistedObjectAdder persistor) +257
   NakedObjects.EntityObjectStore.EntityPersistAlgorithm.MakePersistent(INakedObject nakedObject, IPersistedObjectAdder persistor) +127
   NakedObjects.Persistor.Objectstore.ObjectStorePersistor.MakePersistent(INakedObject nakedObject) +434
   NakedObjects.Web.Mvc.Controllers.NakedObjectsController.ApplyChanges(INakedObject nakedObject, FormCollection form, INakedObjectAssociation parent) +832
   NakedObjects.Web.Mvc.Controllers.GenericControllerImpl.ApplyEdit(ObjectAndControlData controlData, FormCollection form) +164
   NakedObjects.Web.Mvc.Controllers.GenericControllerImpl.Edit(ObjectAndControlData controlData, FormCollection form) +416
   SmsWeb.Controllers.GenericController.Edit(ObjectAndControlData controlData, FormCollection form) in e:\Users\Ury\Projects\LiveNetIl\SMS\SmsWeb\Controllers\GenericController.cs:42
   lambda_method(Closure , ControllerBase , Object[] ) +246
   System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +14
   System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +211
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +27
   System.Web.Mvc.Async.<>c__DisplayClass42.b__41() +28
   System.Web.Mvc.Async.<>c__DisplayClass8`1.b__7(IAsyncResult _) +10
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult) +48
   System.Web.Mvc.Async.<>c__DisplayClass39.b__33() +57
   System.Web.Mvc.Async.<>c__DisplayClass4f.b__49() +223
   System.Web.Mvc.Async.<>c__DisplayClass37.b__36(IAsyncResult asyncResult) +10
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethodWithFilters(IAsyncResult asyncResult) +48
   System.Web.Mvc.Async.<>c__DisplayClass2a.b__20() +24
   System.Web.Mvc.Async.<>c__DisplayClass25.b__22(IAsyncResult asyncResult) +102
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +43
   System.Web.Mvc.<>c__DisplayClass1d.b__18(IAsyncResult asyncResult) +14
   System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +23
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +57
   System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +23
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +47
   System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +10
   System.Web.Mvc.<>c__DisplayClass8.b__3(IAsyncResult asyncResult) +25
   System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +23
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +47
   System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9629708
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155

Coordinator
Nov 12, 2012 at 8:57 AM

"Do you mean I should add the [Disabled] attribute in the class generated by the template?  If I do so, the attribute will disappear after I update the model."  This issue is covered in the manual. See the Warning box in section 2.1.4 for options.

As for your other error, I'm not sure.  If this was based on your previous code, I would say the issue was with your constructor (we don't typically add custom constructors to domain objects with Naked Objects  -  as doing things in the constructor can stuff up Entity Framework)  - but if your code is now being generated by the NO T4 template then there shouldn't be any constructors in there anyway  -  or do you have them in a hand-coded partial class?  If the latter, I suggest you get the basics (CRUD) working without any custom code, before then starting to add behaviour.

Also, once you have the CRUD working, you might well find it better to then switch to Code First  (all you need to do is save the generated classes, delete the .edmx file, and change the Persistor property on RunWeb to the CodeFirst configuration).  ('Code First' is a misnomer by Microsoft:  it does not preclude working from a database first.  'Code First' really means  'no .edmx'.)

 


Nov 12, 2012 at 2:43 PM

As for the attributes - got it.

Regarding the exception: I have no custom code in my project (I commented out and removed the registration of the contributed action above).  All the code is generated either by EF or by NO.

I don't know if it helps, but this is the controlData parameter contents where the exception occurs:

 

controlData	{NakedObjects.Web.Mvc.Models.ObjectAndControlData}	NakedObjects.Web.Mvc.Models.ObjectAndControlData
	ActionAsFinder	null	string
	ActionId	null	string
	Cancel	null	string
	DataDict	Count = 1	System.Collections.Generic.IDictionary {System.Collections.Generic.Dictionary}
	Details	null	string
	Files	Count = 0	System.Collections.Generic.IDictionary {System.Collections.Generic.Dictionary}
	files	Count = 0	System.Collections.Generic.IDictionary {System.Collections.Generic.Dictionary}
	Finder	null	string
	Id	"Model.Customer;1;System.Int64;1;True;;0"	string
	InvokeAction	null	string
	InvokeActionAsFinder	null	string
	InvokeActionAsSave	null	string
	None	null	string
	Pager	null	string
	Redisplay	null	string
	Selector	null	string
	SubAction	None	NakedObjects.Web.Mvc.Models.ObjectAndControlData.SubActionType

 

Coordinator
Nov 12, 2012 at 3:28 PM
ury wrote:
All the code is generated either by EF or by NO.

Code generated by EF is not necessarily correct for NO - it should be generated by the custom NO template. Based on what you posted above this is good for NO and I've tested that it runs fine.

  public class Customer {
        private ICollection<ContentFolder> _ContentFolders = new List<ContentFolder>();
        private ICollection<Session> _Sessions = new List<Session>();
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual bool IsActive { get; set; }
        public virtual string BandwidthHardLimit { get; set; }
        public virtual string SessionHardLimit { get; set; }
        public virtual bool IsSingleUserLimit { get; set; }
        public virtual bool IsSingleIpAddressLimit { get; set; }
        public virtual int CurrentBandwidth { get; set; }
        public virtual int CurrentSessions { get; set; }

        public virtual ICollection<ContentFolder> ContentFolders {
            get { return _ContentFolders; }
            set { _ContentFolders = value; }
        }

        public virtual ICollection<Session> Sessions {
            get { return _Sessions; }
            set { _Sessions = value; }
        }
    }

    public class Session {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
    }

    public class ContentFolder {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
    }

I'd suggest you use this as a starting point and then add more complexity incrementally. I'd also suggest to start with code first.This is the config to run the above code 'code first'. 

 public class RunWeb : RunMvc {
        protected override NakedObjectsContext Context {
            get { return HttpContextContext.CreateInstance(); }
        }

        protected override IServicesInstaller MenuServices {
            get {
                return new ServicesInstaller(new SimpleRepository<Customer>());
            }
        }

        protected override IServicesInstaller ContributedActions {
            get { return new ServicesInstaller(); }
        }

        protected override IServicesInstaller SystemServices {
            get { return new ServicesInstaller(new SimpleEncryptDecrypt()); }
        }

        protected override IObjectPersistorInstaller Persistor
        {
            get
            {
                //Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); //For in-memory database
                Database.SetInitializer(new DropCreateDatabaseIfModelChanges<MyDbContext>()); //Optional behaviour for CodeFirst
                var installer = new EntityPersistorInstaller();
                installer.AddCodeFirstDbContextConstructor(() => new MyDbContext());  //For Code First
                return installer;
            }
        }

        public static void Run() {
            new RunWeb().Start();
        }
    }

    public class MyDbContext : DbContext {

        public DbSet<Customer> Customer { get; set; }
    }

 

 

 

Nov 13, 2012 at 6:57 PM

I took your advice and switched to code-first, and boy was that a long switch...  but I can definitely see signs of life now. 

The problem with heading code-first is that you have to dive deep into EF and NO, and spend your time customizing classes instead of quickly drawing SSMS diagrams or entity model diagrams.  I fear I will spend a lot of time on "Seeding" indexes and figuring out fluent API...

Editor
Nov 13, 2012 at 7:01 PM
We're also converting from .edmx to code first.

I've not tried it (yet), but there are the power tools [1] which will create a read-only EF diagram for you from your DBContext. These are (apparently) compatible with all releases after EF 4.2 [2]

And you can, of course, generate a UML class diagram (though I guess that might depend on the version of VS that you are using).

Dan

Jun 13, 2013 at 10:12 AM
Hi Guys,
            I think it would be nice if NO worked the way that our Newbie expected it to as well.
I thought from memory that NO supported at least one level of adding contributed actions for object parameters. Otherwise the user could get a bit lost but I think that it would be ok, if that is what the user required.
In my case I would like the methods to find an office to be injected when I want to look for a WorkItem
[MemberOrder(3)]
        public IQueryable<WorkItem> FindAvailableWorkItemsByOffice(Office office) // Will be injected in the action menu not the find menu unfortunately NO works this way!!
        {
            IQueryable<WorkItem> query = from obj in Instances<WorkItem>()
                                         // NB can not use obj.IsWorkItemAvailable() as Linq to Entities does not support object methods
                                         where obj.WorkItemState != WorkItemState.Active &&
                                               obj.WorkItemState != WorkItemState.Cancelled &&
                                               obj.Office.OfficeId == office.OfficeId
                                               
                                         select obj;

            return query;
        }

        [MemberOrder(4)]
        public IQueryable<WorkItem> FindAvailableWorkItemsByOffices(IQueryable<Office> offices) 
        {
            IQueryable<WorkItem> query = from obj in Instances<WorkItem>()
                                         // NB can not use obj.IsWorkItemAvailable() as Linq to Entities does not support object methods
                                         where obj.WorkItemState != WorkItemState.Active &&
                                               obj.WorkItemState != WorkItemState.Cancelled &&
                                               offices.Any(x => x.OfficeId == obj.Office.OfficeId)
                                         select obj;

            return query;
        }