The Ed-Fi “Classic Dashboards” are no longer supported through the Ed-Fi Alliance. You can still receive support and maintenance through the Ed-Fi vendor community. Please look at any of the vendors’ dashboard solutions on the Registry of Ed-Fi Badges or the Ed-Fi Starter Kits if you are looking for a visualization solution to use with the Ed-Fi ODS. This documentation will remain available to assist existing Classic Dashboard implementers.

Plugin Architecture - Teacher’s Student Notes (CRUD Example)

This section builds upon the example in the previous section, Plugin Architecture - Hello World (Sample Plugin). We recommend that you run through that exercise before continuing on to the example on this page.

Now that you know the basics of how to develop a plugin for the Ed-Fi Dashboards, we can take the concepts further and implement a full-featured plugin. In this example, we will create a plugin that enables teachers to take notes regarding a given student. This example covers a wide range of typical plugin development activities, including creating persistent storage, form validation, server-side validation, custom plugin services, custom resources, and custom route registration.

From an Agile development perspective, the notes feature in this exercise involves five stories:

  • Adding an entry into the student level menu for notes.
  • Creating a view where a teacher can input notes for a student. This form should be validated.
  • Creating a view where a teacher can list all notes for a student.
  • Allowing for teachers to be able to edit a note.
  • Allowing for teachers to delete a note.

The code for this walkthrough can be found in Ed-Fi-Samples Repository Sample Plugins/TeacherNotes.

Teacher's Student Notes: Initial Web Artifacts

Start a new plugin by having the main controller, view, and plugin registered in the menu. This will put in the basic functionality in place to start and build upon. Similar to the Hello World example, create an MVC web application and name it "EdFi.Dashboards.Plugins.TeacherNotes.Web".

  1. Attach this plugin at the student school level. Create the following paths: \Areas\StudentSchool\Controllers and \Areas\StudentSchool\Views\Notes.
  2. Create the NotesController in the Controllers folder with the standard Get action result method. For security reasons, the signature of the method should have the context values SchoolId and StudentUSI.

    using EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool;
    using EdFi.Dashboards.Plugins.TeacherNotes.Resources.Navigation.MVC.Areas;
    using EdFi.Dashboards.Plugins.TeacherNotes.Resources.StudentSchool;
    using System.Web.Mvc;
     
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Web.Areas.StudentSchool.Controllers 
    { 
    	public class NotesController : Controller 
    	{ 
    		private readonly INotesService _service; 
    		private readonly IStudentSchoolAreaLinks _studentSchoolAreaLinks;
    		public NotesController(INotesService service, IStudentSchoolAreaLinks studentSchoolAreaLinks)
    		{ 
    			this._service = service;
    			this._studentSchoolAreaLinks = studentSchoolAreaLinks;
    		}
    		[HttpGet]
    		public ActionResult Get(int schoolId, long studentUSI, int? id) 
    		{ 
    			var listContext = Request.QueryString["listContext"];
    			if (!id.HasValue) //collection 
    			{ 
    				var notesModel = this._service.Get(new NotesRequest() {SchoolId = schoolId, StudentUSI = studentUSI, ListContext = listContext});
    				return View(notesModel); 
    			} 
    				var noteModel = this._service.Get(new NoteModel() { SchoolId = schoolId, StudentUSI = studentUSI, Id = id.Value, ListContext = listContext});
    				 return View("Details", noteModel); 
    			} 
    	} 
    }
  3. Create the corresponding Get View. Do not forget to add the RazorGenerator tool so it can create a compiled version of the View.

    @using System.Web.Mvc.Html 
    @using Cassette.Views 
    @using EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool 
    @using EdFi.Dashboards.Presentation.Core.Plugins.Helpers 
     
    @model EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool.NotesModel 
    @{ 
    	ViewBag.Title = "Student Notes";
    	Bundles.Reference(CassetteHelper.GetStyleBundleName()); 
    }
    @section ContentPlaceHolderHead {
    	<script type="text/javascript"> 
    		var pageArea = { Area: 'student', Page: 'Student Notes' };
    	</script> }
    @section ContentPlaceHolder1 {
    	@Html.Partial("_Alert")
    	<h2>Student Notes</h2>
    	  <div id="noteList">
    	    <a id="createNote" class="btn" style="float: right">
    		  <i class="icon-plus"></i> Add Note</a>
    		  <p class="inline-links">Note management.</p>
     		  <table id="notes-table" class="standard-table standard-table-compressed" summary="Table displays data related to student notes.">
    		    <thead>
    			  <tr>
    				 <th>Date</th>
    				 <th>Description</th>
     				 <th>Details</th>
    				 <th>Edit</th>
    				 <th>Delete</th>
    			 </tr>
    		   </thead>
    	       <tbody>
    			 @if (Model.NoteModels.Any())
    			 { 
    				for (int i = 0; i < Model.NoteModels.Count(); i++)
    				 { 
    					var note = Model.NoteModels.ElementAt(i);
    					 <tr class="@((i%2 == 0) ? "" : "alt")" data-resource-id="@note.Id">
    					  <td>@(note.DateTime.ToShortDateString())</td>
    					  <td>@(note.Description)</td> <td><a href="@note.ResourceUrl">
    						<i class="icon-vcard" title="View Detail"></i></a>
    					  </td>
    					  <td><a href="@note.ResourceUrl#Edit"><i class="icon-pencil" title="Edit Note"></i></a></td>
    					  <td><a class="delete" href="#" data-resource-id="@note.Id" data-resource-url="@note.ResourceUrl">
    						  <i class="icon-cancel" title="Delete Note"></i></a>
    					  </td>
    					 </tr>
    				 }
    
    
  4. Create the PluginManifest class.

    using System.Collections.Generic;
    using System.Web;
    using EdFi.Dashboards.Core.Providers.Context;
    using EdFi.Dashboards.Resource.Models.Common;
    using EdFi.Dashboards.Resources.Models.Plugins;
    using EdFi.Dashboards.Resources.Navigation;
    using EdFi.Dashboards.Resources.Navigation.Support;
    using EdFi.Dashboards.Resources.Plugins;
     
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Web 
    { 
    	public class PluginManifest : IPluginManifest 
    	{ 
    		private readonly IStudentSchoolAreaLinks _studentSchoolAreaLinks;
    		public PluginManifest(IStudentSchoolAreaLinks studentSchoolAreaLinks)
    		{ 
    			this._studentSchoolAreaLinks = studentSchoolAreaLinks; 
    		} 
    			/// <summary> 
    			/// Name of the plugin 
    			/// </summary>
    			public string Name { get { return this.GetType().Assembly.GetName().Name; } }
    			/// <summary> 
    			/// Version of the plugin 
    			/// </summary> 
    			public string Version { get { return this.GetType().Assembly.GetName().Version.ToString(); } }
    			/// <summary> 
    			/// Menu definition for the plugin 
    			/// </summary> 
    			public IEnumerable<PluginMenu> PluginMenus 
    			{ 
    				get { return GetPluginMenus(); } 
    			} 
    			private IEnumerable<PluginMenu> GetPluginMenus()
    			{ 
    				var listContext = HttpContext.Current.Request.QueryString["listContext"];
    				var requestContext = EdFiDashboardContext.Current; 
    				if ((!requestContext.SchoolId.HasValue) || (!requestContext.StudentUSI.HasValue)) 
    					return new List<PluginMenu>();
    				return new List<PluginMenu> 
    				{ 
    					new PluginMenu 
    					{ 
    						Area = "StudentSchool", 
    						ResourceModels = new List<ResourceModel> 
    						{ 
    							new ResourceModel { Text = "Teacher Notes",
  5. Add a reference to Castle Windsor.

  6. Create the marker interface so you can reference the plugin assembly.

    namespace EdFi.Dashboards.Plugins.TeacherNotes.Web 
    { 
    	/// <summary> 
    	/// Marker interface for the EdFi.Dashboards.Plugins.TeacherNotes.Web assembly. 
    	/// </summary> 
    	public interface Marker_EdFi_Dashboards_Plugins_TeacherNotes_Web 
    	{ 
    	} 
    }
  7. Add the web installer so you can register the Plugin Manifest.

    using Castle.MicroKernel.Registration; 
    using Castle.MicroKernel.SubSystems.Configuration; 
    using Castle.Windsor; using EdFi.Dashboards.Presentation.Core.Plugins.Utilities.CastleWindsor; 
    using EdFi.Dashboards.Resources.Plugins; 
     
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Web.Utilities.CastleWindsor 
    { 
    	public class Installer : WebDefaultConventionInstaller<Marker_EdFi_Dashboards_Plugins_TeacherNotes_Web> 
    	{ 
    		public override void Install(IWindsorContainer container, IConfigurationStore store) 
    		{ 
    			base.Install(container, store);
    			container.Register(Component 
    				.For<IPluginManifest>() 
    				.ImplementedBy<PluginManifest>()); 
    		} 
    	}
    }
  8. Compile the code and run the project. Once you navigate to a student level, you should see the following: the menu integration, the route, and working view.

Data Components

The Teacher's Student Notes in this example requires persistent storage to save the information a teacher enters. This section outlines the database modifications necessary, along with the companion coding steps required.

  1. Create the database structure needed to persist the notes. For this example you will be adding a new table and schema to the EdFi_Application database, because this database is a persistent store for the dashboard.
    1. Create the new schema.

      CREATE SCHEMA [teachernotes] AUTHORIZATION [dbo]
    2. Give the appropriate user access to that schema. For this example you are using the edfiPService user.

      GRANT SELECT ON SCHEMA::[teachernotes] TO [edfiPService]
      GRANT INSERT ON SCHEMA::[teachernotes] TO [edfiPService]
      GRANT UPDATE ON SCHEMA::[teachernotes] TO [edfiPService]
      GRANT DELETE ON SCHEMA::[teachernotes] TO [edfiPService]
      GO
    3. Create the Note table.

      CREATE TABLE [teachernotes].[Note](
      	[NoteId] [int] IDENTITY(1,1) NOT NULL,
      	[SchoolId] [int] NOT NULL,
      	[StudentUSI] [bigint] NOT NULL,
      	[Description] [nvarchar](max) NOT NULL,
      	[Date] [datetime] NOT NULL,
      CONSTRAINT [PK_Note] PRIMARY KEY CLUSTERED
      (
      	[NoteId] ASC
      ) ON [PRIMARY]
      ) ON [PRIMARY]
  2. Create a plugin data project named EdFi.Dashboards.Plugins.TeacherNotes.Data to consume the data structures.

  3. Add references to dependencies: System.Data.Linq, EdFi.Dashboards.Presentation.Core, Subsonic, and Castle.Windsor

  4. Copy the Entities folder, including its content and the app.config from the EdFi.Dashboards.Extensions.Data project.

  5. Change the app.config connection string to point to the EdFi_Application database.

  6. Change the Settings.tt include file in the following ways:

    1. The IncludedSchemas array needs to be modified so that it only has the schema you created.

       string[] IncludedSchemas = new string[] { "teachernotes" };
    2. The Namespace string needs to be updated to reflect the plugin we are working on.

      const string Namespace = "EdFi.Dashboards.Plugins.TeacherNotes.Data.Entities";
  7. Run the t4 templates by right-clicking on POCOSObjects.tt, choosing the “Run Custom Tool” option, and reviewing the generated POCOSObjects.cs file. It should include the POCOS representation of our Note table. 

  8. Add a marker interface to the Data project.

    namespace EdFi.Dashboards.Plugins.TeacherNotes.Data 
    { 
    	/// <summary> 
    	/// Marker interface for the EdFi.Dashboards.Plugins.TeacherNotes.Data assembly. 
    	/// </summary> 
    	public interface Marker_EdFi_Dashboards_Plugins_TeacherNotes_Data { }
    }
  9. Create an Installer class for the data project.

    1. Create the folder structure Utilities\CastleWindsor\.

    2. Add the class file PersistedRepositoryInstaller.cs. The class should implement PersistedRepositoryDefaultConventionInstaller using your marker interface.

using EdFi.Dashboards.Presentation.Core.Plugins.Utilities.CastleWindsor; 
 
namespace EdFi.Dashboards.Plugins.TeacherNotes.Data.Utilities.CastleWindsor 
{ 
public class PersistedRepositoryInstaller : PersistedRepositoryDefaultConventionInstaller<Marker_EdFi_Dashboards_Plugins_TeacherNotes_Data> 
{ 
} 
}

Custom Route Registration and Installation

Sometimes it is necessary to register custom routes that are not provided in the core application. The Ed-Fi dashboards provide an extensibility point through implementing the IAreaRouteMappingPreparer and registering the implementation in the IoC container.

Create a Custom Route

For this example plugin we will need to create a custom route that looks like this:

LocalEducationAgencyAreaRegistration.LocalEducationAgencyPrefix + "/{localEducationAgency}/Schools/{school}/Students/{student}-{studentUSI}/{controller}/{id}";

To create the custom route:

  1. In the EdFi.Dashboards.Plugins.TeacherNotes.Web project, under the Areas\StudentSchool\ folder create a new class named StudentSchoolAreaRouteMappingPreparer that implements IAreaRouteMappingPreparer.

Implement the method CustomizeRouteMappings by extending the route mappings to contain your custom route.

using System; 
using System.Collections.Generic; 
using EdFi.Dashboards.Presentation.Architecture.Mvc.AreaRegistration; 
using EdFi.Dashboards.Presentation.Core.Areas.LocalEducationAgency; 
using EdFi.Dashboards.Presentation.Web.Utilities; 
 
namespace EdFi.Dashboards.Plugins.TeacherNotes.Web.Areas.StudentSchool 
{ 
	public class StudentSchoolAreaRouteMappingPreparer : IAreaRouteMappingPreparer 
	{ 
		private readonly string _customRouteUrl = LocalEducationAgencyAreaRegistration.LocalEducationAgencyPrefix + 	"/{localEducationAgency}/Schools/{school}/Students/{student}-{studentUSI}/{controller}/{id}"; 
		public void CustomizeRouteMappings(string areaName, List<RouteMapping> routeMappings) 
		{ 
			if (string.Compare(areaName, "StudentSchool", StringComparison.InvariantCultureIgnoreCase) == 0) 
			{ 
				routeMappings.Add(new RouteMapping( 
					"StudentSchool_Resources_with_Id", 
					_customRouteUrl, 
					new {action = "Get", controller = "Overview"}, 
					new 
					{ 
						localEducationAgency = RoutingPatterns.LocalEducationAgency, 
						school = RoutingPatterns.School, 
						student = RoutingPatterns.Student, 
						studentUSI = RoutingPatterns.Id, 
						controller = RoutingPatterns.Resource, 
						id = RoutingPatterns.Id, 
					} 
					)); 
			} 
		} 
	} 
}

Create an Extension to Support the New Route

Following the Ed-Fi Dashboard architecture patterns, we'll use an established abstraction for generating our resource URLs. In the EdFi.Dashboards.Resources\Navigation\MVC folder, you will find multiple classes that enable you to easily create resource URLs. To comply with the current requirement and to extend the routes, we will create an extension to support the new route:

  1. Create a new project named EdFi.Dashboards.Plugins.TeacherNotes.Resources.
  2. Create new folders “Navigation\MVC\Areas” and “StudentSchool” off the root.
  3. In the Navigation\MVC\Areas folder, create a StudentSchool class that implements IStudentSchoolAreaLinks. This will be used for the creation of the resource’s URIs.

    using System.Reflection; 
    using EdFi.Dashboards.Resources.Navigation; 
     
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Resources.Navigation.MVC.Areas 
    { 
    	public interface IStudentSchoolAreaLinks 
    	{ 
    		string Notes(int schoolId, long studentUSI, string student = null, int? id = null, object additionalValues = null); 
    	} 
    	public class StudentSchool : SiteAreaBase, IStudentSchoolAreaLinks 
    	{ 
    		public virtual string Notes(int schoolId, long studentUSI, string student = null, int? id = null, object additionalValues = null) 
    		{ 
    			if (id == null) 
    			{ 
    				return BuildResourceUrl(additionalValues, MethodBase.GetCurrentMethod(), schoolId, studentUSI, student); 
    			} return BuildUrl("StudentSchool_Resources_with_Id", additionalValues, MethodBase.GetCurrentMethod(), schoolId, studentUSI, student, id); 
    		} 
    	} 
    }
  4. In the EdFi.Dashboards.Plugins.TeacherNotes.Web project, under the Utilities\CastleWindsor folder, create a ConfigurationSpecificInstaller class to register previous RouteMappingPreparer and IStudentSchoolAreaLinks implementations.
using Castle.MicroKernel.Registration; 
using Castle.Windsor; 
using EdFi.Dashboards.Common.Utilities.CastleWindsorInstallers; 
using EdFi.Dashboards.Plugins.TeacherNotes.Resources.Navigation.MVC.Areas; 
using EdFi.Dashboards.Presentation.Architecture.Mvc.AreaRegistration; 
using System.Linq; 
using System.Reflection; 
 
namespace EdFi.Dashboards.Plugins.TeacherNotes.Web.Utilities.CastleWindsor 
{ 
	public class ConfigurationSpecificInstaller : RegistrationMethodsInstallerBase 
	{ 
		protected virtual void RegisterAreaRouteMappingPreparer(IWindsorContainer container) 
		{ 
			var assemblyTypes = Assembly.GetExecutingAssembly().GetTypes(); 
			var filterTypes = 
				(from t in assemblyTypes 
					where typeof 
					(IAreaRouteMappingPreparer).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass select t).ToList();
					foreach (var type in filterTypes) 
					{ 
						container.Register(Component 
							.For<IAreaRouteMappingPreparer>() 
							.ImplementedBy(type)); 
					} 
		} 
		protected virtual void RegisterSiteAreaLinks(IWindsorContainer container) 
		{ 
			container.Register(Component 
				.For<IStudentSchoolAreaLinks>() 
				.ImplementedBy<StudentSchool>()); 
		} 
	} 
}

Resource Services

Now that we have the basic artifacts and the data components in place, we'll continue by developing the supporting service, request model, and resource model.

  1. Create the EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models project.
  2. Add a StudentSchool folder and add the ~\StudentSchool\NoteModel.cs class file.
  3. Create a NoteModel class. This class will implement the IResourceModelBase interface and define the fields needed to represent the Notes object. Also add in a RedirectUrl property as this be used later on for additional navigation.

    using System; 
    using System.Collections.Generic; 
    using System.Runtime.CompilerServices; 
    using EdFi.Dashboards.Resource.Models.Common.Hal; 
    using EdFi.Dashboards.Resources.Security.Common; 
    using Newtonsoft.Json; 
    using Newtonsoft.Json.Schema; 
    using System.ComponentModel.DataAnnotations; 
     
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool 
    { 
    	[Serializable] 
    	public class NoteModel : IResourceModelBase 
    	{ 
    		private string _resourceUrl; 
    		private string _redirectUrl; 
    		private string _url; 
    		public NoteModel() 
    		{ 
    			Links = new Links(); 
    		} 
    		public long StudentUSI { get; set; } 
    		public int SchoolId { get; set; } 
    		[AuthenticationIgnore("Irrelevant")] 
    		public string ListContext { get; set; } 
    		[AuthenticationIgnore("Irrelevant")] 
    		public int? Id { get; set; } 
    		[AuthenticationIgnore("Irrelevant")] 
    		public DateTime DateTime { get; set; } 
    		[Required] 
    		[AuthenticationIgnore("Irrelevant")] 
    		public string Description { get; set; } 
    		[JsonIgnore] 
    		[AuthenticationIgnore("Irrelevant")] 
    		public string RedirectUrl 
    		{ 
    			get { return _redirectUrl; } 
    			set { _redirectUrl = value; Links.AddLink("redir", value); } 
    		} 
    		[JsonIgnore] 
    		[AuthenticationIgnore("Irrelevant")] 
    		public string ResourceUrl 
    		{ 
    			get { return _resourceUrl; } 
    			set { _resourceUrl = value; Links.self = new Link { Href = value }; } 
    		} 
    		[JsonIgnore] 
    		[AuthenticationIgnore("Irrelevant")] 
    		public string Url { get { return _url; }
     
  4. Mark the Description property as required, using the System.ComponentModel.DataAnnotations.Required attribute.

  5. Create a NotesModel class that inherits from ResourceModelBase. This model will be used to return a list containing all notes for a student.

    [Serializable] 
    	public class NotesModel : ResourceModelBase 
    	{ 
    		public NotesModel() 
    		{ 
    			NoteModels = new List<NoteModel>(); 
    		} 
    		public IEnumerable<NoteModel> NoteModels { get; set; } 
    	} 
    }
  6. Add a reference to the Models project into the EdFi.Dashboards.Plugins.TeacherNotes.Resources project.

  7. In the EdFi.Dashboards.Plugins.TeacherNotes.Resources project, add a StudentSchool folder and a NotesService class in that folder.

  8. The NotesService class will handle all of the requests to create, edit, read and delete a single note as well as to display a list of all of the notes for a given student.

    1. Create a NotesRequest class that has the properties of SchoolId and StudentUSI. This class will be used for retrieving all the notes for a given student.

      public class NotesRequest 
      { 
      	public int SchoolId { get; set; } 
      	public long StudentUSI { get; set; } 
      	[AuthenticationIgnore("Irrelevant")] 
      	public string ListContext { get; set; } 
      }
    2. Create the INotesService interface. This interface will implement the RESTful services handlers of IPostHandler, IPutHandler, and IDeleteHandler, plus the two Get handlers of IService.

      public interface INotesService : 
      	IService<NotesRequest, NotesModel>, 
      	IService<NoteModel, NoteModel>, 
      	IPostHandler<NoteModel, string>, 
      	IPutHandler<NoteModel, string>, 
      	IDeleteHandler<NoteModel>
    3. The INotesService interface is then used as the interface for the NotesService class.
    4. Following the RESTful patterns established for the Ed-Fi Dashboards, the resources should have URIs. To create the URIs, we will use the provided IStudentSchoolAreaLinks, which requires implementation of two Get methods. One that takes in a NotesRequest and returning; the other taking in a NoteRequest. These methods retrieve the requested data from the repository and return a NoteModel object with the correct properties filled out.

      The Get methods need to be decorated with the CanBeAuthorizedBy attribute and required claims, EdFiClaimTypes.ViewAllStudents and EdFiClaimTypes.ViewMyStudents.

      public class NotesService : INotesService 
      { 
      	private readonly IPersistingRepository<Note> noteRepository; 
      	private readonly IStudentSchoolAreaLinks studentSchoolAreaLinks; 
      	public NotesService(IPersistingRepository<Note> noteRepository, IStudentSchoolAreaLinks studentSchoolAreaLinks) 
      	{ 
      		this.noteRepository = noteRepository; 
      		this.studentSchoolAreaLinks = studentSchoolAreaLinks; 
      	} 
      	[NoCache] 
      	[CanBeAuthorizedBy(EdFiClaimTypes.ViewAllStudents, EdFiClaimTypes.ViewMyStudents)] 
      	public NotesModel Get(NotesRequest request) 
      	{ 
      		var data = (from n in this.noteRepository.GetAll() 
      			where n.SchoolId == request.SchoolId && 
      				n.StudentUSI == request.StudentUSI select n).ToList(); 
      		// Map data to resource model. 
      		var model = new NotesModel 
      		{ 
      			NoteModels = data.Select(dataNote => new NoteModel 
      			{ 
      				Id = dataNote.NoteId, 
      				Description = dataNote.Description, 
      				DateTime = dataNote.Date, 
      				ResourceUrl = this.studentSchoolAreaLinks.Notes(request.SchoolId, request.StudentUSI, null, dataNote.NoteId), 
      				RedirectUrl = this.studentSchoolAreaLinks.Notes(request.SchoolId, request.StudentUSI).AppendParameters("listContext=" + request.ListContext), }), 
      				ResourceUrl = this.studentSchoolAreaLinks.Notes(request.SchoolId, request.StudentUSI).AppendParameters("listContext=" + request.ListContext), }; 
      				return model; 
      }
    5. Implement a Put method to update a given note.

      [NoCache]
      [CanBeAuthorizedBy(EdFiClaimTypes.ViewAllStudents, EdFiClaimTypes.ViewMyStudents)] 
      public string Put(NoteModel request, out bool created) 
      { 
      	var note = 
      		this.noteRepository.GetAll() .SingleOrDefault( 
      			x => (x.NoteId == request.Id) && (x.SchoolId == request.SchoolId) && (x.StudentUSI == request.StudentUSI)); 
      		//if not create a new record 
      		if (note != null) 
      		{ 
      			//or update the existing record 
      			note.Description = request.Description; 
      			note.Date = DateTime.Now; 
      			this.noteRepository.Save(note, out created); 
      			return "Success"; 
      		} 
      		created = false; 
      		return "Failure"; 
      }
    6. Implement a Post method to create new notes.

      [NoCache] 
      [CanBeAuthorizedBy(EdFiClaimTypes.ViewAllStudents, EdFiClaimTypes.ViewMyStudents)] 
      public string Post(NoteModel request) 
      { 
      	var note = new Note 
      		{ 
      			StudentUSI = request.StudentUSI, 
      			SchoolId = request.SchoolId, 
      			Date = DateTime.Now, 
      			Description = request.Description, 
      		}; 
      		var created = true; 
      		this.noteRepository.Save(note, out created);
      		return "Success";
      }
    7. Implement a Delete method that deletes the specified note.

      [NoCache]
      [CanBeAuthorizedBy(EdFiClaimTypes.ViewAllStudents, EdFiClaimTypes.ViewMyStudents)]
      public void Delete(NoteModel request) 
      { 
      	this.noteRepository.Delete(x => 
      		(x.NoteId == request.Id) && 
      		(x.SchoolId == request.SchoolId) && 
      		(x.StudentUSI == request.StudentUSI));
      }
  9. The last thing you need to do with the Resources project is create a CastleWindsor installer for the assembly. This has two parts.

    1. Create a ResourcesInstaller class that inherits from the ResourcesDefaultConventionInstaller class.

    2. The ResourcesDefaultConventionInstaller class wires up the IService, IPutHandler, and IDeleteHandler so it is important to use that installer class when relying on those interfaces.

      using EdFi.Dashboards.Presentation.Core.Plugins.Utilities.CastleWindsor; 
       
      namespace EdFi.Dashboards.Plugins.TeacherNotes.Resources.Utilities.CastleWindsor 
      { 
      	public class ResourcesInstaller : ResourceDefaultConventionInstaller<Marker_EdFi_Dashboards_Plugins_TeacherNotes_Resources> 
      	{ 
      	} 
      }

Controllers and Views

Now we'll create the views and wire up the controller to talk to those views and the NotesService class.

The views need to have the correct namespace, which follows the pattern EdFi.Dashboards.Plugins.[PluginName].Web.Areas.[area].Views.[controllerName].[viewname].

Creating a Partial View for Editing/Creating Notes

The first thing you will do is create a partial view for editing/creating of the notes themselves.

  1. Under the Areas\Views\Notes folder of the Web project, create an MVC Partial Page named Edit.cshtml.
  2. Set the Custom Tool property on the page to the RazorGenerator tool.
  3. Add the code to give the user a TextArea to enter the Description. Make Description (i.e., the note content) a required field, and add a "Save" button.

    @using System.Web.Mvc 
    @using System.Web.Mvc.Html 
     
    @model 
    EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool.NoteModel 
     
    @using (Html.BeginForm("Post", "Notes", FormMethod.Post, new { action = Model.Url, 
    id = "form-note", @class = "standard-form myplugin-form" })) 
    { 
    <div id="note-container" class="note-container"> <p> 
    @Html.LabelFor(m => m.Description)<a id="back" class="btn" style="float: right" onclick="cancelButton();"><i class="icon-cancel"></i></a>
    @Html.TextAreaFor(m => m.Description, new { required = "required" }) 
    @Html.ValidationMessageFor(m => m.Description) </p> <input type="submit" value="Save" /> 
    </div> 
    }
  4. Next you will add JavaScript to submit the form using jQuery.ajax syntax. This allows you to utilize the NotesService IPutHandler code.

    <script             vascript">
    //EdFi-Standard-Alert plugin.
    var edfiAlert = new $.fn.edFiStandardAlert();
    $("#form-note").submit(function () {
    
    var httpVerb = '@(Model.Id.HasValue ? "PUT" :"POST" )';
    //Make sure form is valid before continuing. if (!$("#form-note").valid())
    return false;
    var serializedFormData = $("#form-note").serializeFormToJSON();
    $.ajax({
    headers: {
    accept: "application/json; charset=utf-8",
    },
    contentType: 'application/json; charset=utf-8', url: '@Model.Url',
    type: httpVerb,
    data: JSON.stringify(serializedFormData), success: function (data) {
    edfiAlert.show(edfiAlert.status.success, "The note has been added.", "@Model.ResourceUrl");
    },
    error: function(request, status, error) { edfiAlert.show(edfiAlert.status.error, "Some error
    occured please review and try again. " + request.responseText);
    }
    });
    });
    //Do not submit form. return false;
    function cancelButton() {
    if ($('#noteList').length > 0) {
    $('#newNote').hide('slide', { direction: 'up' }, '600',
    function () { $('#noteList').show('slide', { direction: 'up' }, '600'); });
    } else {
    window.location.href = '@Model.RedirectUrl';
    }
    }
    </script>
  5. Now that we're finished with the partial view, we'll add the code to the Get view to display the list of notes and add a new note. Note that we're using @Html.Partial to render the Edit view into the Get view for the creation of notes and that we pass a new NoteModel object into the partial view.

    @section ContentPlaceHolder1 {
    @Html.Partial("_Alert")
    <h2>Student Notes</h2>
    <div id="noteList">
    <a id="createNote" class="btn" style="float: right"><i class="icon- plus"></i> Add Note</a>
    <p class="inline-links"> Note management.
    
    </p>
    <table id="notes-table" class="standard-table standard-table- compressed" summary="Table displays 
    data related to student notes.">
    <thead>
    <tr>
    </tr>
    </thead>
    <tbody>
    <th>Date</th>
    <th>Description</th>
    <th>Details</th>
    <th>Edit</th>
    <th>Delete</th>
    @if (Model.NoteModels.Any())
    {
    
    i++)
    data-resource-id="@note.Id">
    for (int i = 0; i < Model.NoteModels.Count();
    {
    var note = Model.NoteModels.ElementAt(i);
    <tr class="@((i%2 == 0) ? "" : "alt")"
    <td>@(note.DateTime.ToShortDateString())</td>
    <td>@(note.Description)</td>
    <td><a href="@note.ResourceUrl"><i class="icon-vcard" title="View Detail"></i></a></td>
    <td><a href="@note.ResourceUrl#Edit"><i class="icon-pencil" title="Edit Note"></i></a></td>
    <td><a class="delete" href="#" data-resource-id="@note.Id" data-resource-url="@note.ResourceUrl"><i 
    class="icon- cancel" title="Delete Note"></i></a></td>
    }
    }
    else
    {
    
    </tr>
    
    <tr><td colspan="5">
    <span class="MetricNoData">No data currently
    available.</span>                 </td></tr>
    }</tbody></table></div>
    <div id="newNote" style="display: none">
    @Html.Partial("Edit", new NoteModel { Resourc
    @Html.Partial("Edit", new NoteModel { Resour
    Model.ResourceUrl, RedirectUrl = Model.ResourceUrl})</div>
  6. Next you will add the JavaScript to wire up the View’s client-side behavior. Again you are using jQuery.ajax to handle the delete action.

    function DeleteNote(resourceRoute, resourceId) {
    $.ajax({
    type: "DELETE",
    url: resourceRoute, success: function() {
    //Remove row from UI.
    var $row = $('tr[data-resource-id=' + resourceId + ']');
    $row.slideUp("slow", function () {
    $row.remove(); fixRowAlternatingColors();
    });
    been deleted.");
    },
    
    edfiAlert.showDelayHide(edfiAlert.status.success, "The note has
    error: function (request, status, error) { edfiAlert.show(edfiAlert.status.error, "Some error 
    occured please
    review and try again. " + request.responseText);
    }
    });
    }
    function fixRowAlternatingColors() {
    //Clean start
    $('#notes-table tbody tr').removeClass("alt");
    //Re-stripe
    $('#notes-table tbody tr:odd').addClass("alt");
    }
    </script>
  7. The user interface for this example requires a Details View that will be used for the display and editing of existing notes. Create a new view called "Details" under the Areas\StudentSchool\View\Notes folder of the Web project.

  8. Add code to display a given note with a partial view to edit the given note depending on how the page is called.
@using System.Web.Mvc.H
@using Cassette.Views
@using EdFi.Dashboards.Presentation.Core.Plugins.Helpers
@model EdFi.Dashboards.Plugins.TeacherNotes.Resources.Models.StudentSchool.NoteModel
@{
ViewBag.Title = "Student Note"; Bundles.Reference(CassetteHelper.GetStyleBundleName());
}

@section ContentPlaceHolderHead {
<script type="text/javascript">
var pageArea = { Area: 'student', Page: 'Student Note' };
</script>
}
@section ContentPlaceHolder1 {
@Html.Partial("_Alert")
<h2>Student Note</h2>
@if (Model == null)
{
}
else
{
<span class="MetricNoData">No Note currently available.</span>
<div id="viewNote" class="note-detail">
<p class="title">
<label>Date:</label> @Model.DateTime

</div>
</p>
<p>
</p>

<label>Description:</label> @Model.Description
<div id="editNote" style="display: none">
@Html.Partial("Edit", Model)
</div>
}

Wiring Up the Connections

Now that we have the views, we need to wire up the connection between the views, the controller, and the NotesService class.

  1. Add a constructor in the controller that takes in an INotesService parameter and an IStudentSchoolAreaLinks parameter.
  2. In the Get method of the controller make a call to the NotesService to retrieve the correct object or objects. As you can see in the code snippet below, if an Id is present we want the plugin to render the Details view, otherwise render the Get view.

    [HttpGet]
    public ActionResult Get(int schoolId, long studentUSI, int? id)
    {
    
    var listContext = Request.QueryString["listContext"];
    if (!id.HasValue) //collection
    {
    var notesModel = this._service.Get(new NotesRequest()
    {SchoolId = schoolId, StudentUSI = studentUSI, ListContext = listContext});
    return View(notesModel);
    
    }
    var noteModel = this._service.Get(new NoteModel() { SchoolId = schoolId, StudentUSI = studentUSI, 
    Id = id.Value, ListContext = listContext});
    return View("Details", noteModel);
    
    }

Bundling Assets (CSS, Images, and JavaScript Files)

The Ed-Fi Dashboards use Cassette (http://getcassette.net/) to bundle all of the JavaScript, CSS, and image files for faster client performance. For plugins, there is an extensibility option provided that will also use Cassette to bundle the plugin’s web resources.

  1. Add some CSS files and add a NuGet reference to Cassette and Cassette.Views. Create a "Content" folder in the Web project and add a "Scripts" and "StyleSheets" folder underneath that.
  2. Add the following code for the Style.css and Style.scss files and mark those CSS files as an Embedded Resource under the Build Action property of the file.

    .myplugin-form .note-container {
      margin: 0 auto; 
      width: 500px; }
    .myplugin-form label { font-weight: bold; text-align: left; }
    .myplugin-form input[type="text"], .myplugin-form textarea { border: 2px solid Gray; }
    .myplugin-form input[type="submit"] { margin: 0 auto; }
    .myplugin-form textarea { width: 100%;
    height: 120px; }
    .standard-table i { font-size: 16px; }
    .note-detail { font-size: 9pt; color: #000;
    background-color: #d2e2ef; width: 97%;
    border-radius: 5px; padding: 10px; display: -moz-block; display: block; zoom: 1;
    vertical-align: top; }
    .note-detail .title { font-weight: bold;
    12pt;
    12pt;
    color: #255b80; }
  3. In the root of the plugin’s Web project, add a class for the CassetteConfigurations that inherits from CassetteDefaultConventionBundleConfiguration and uses the marker interface as the type.

    using EdFi.Dashboards.Presentation.Core.Plugins.Utilities;
    namespace EdFi.Dashboards.Plugins.TeacherNotes.Web
    {
    	/// <summary>
    	/// Configures the Cassette asset bundles for the web application.
    	/// </summary>
    	public class CassetteConventionBundleConfiguration :
    		CassetteDefaultConventionBundleConfiguration<Marker_EdFi_Dashboards_Plugins_TeacherNotes_Web>
    	{
    	}
    }
  4. In all views, we need to specify a reference to the bundle. This is done by adding the line, Bundles.Reference(CassetteHelper.GetStyleBundleName()); into the view.
    You should now be able to go back to your student and look at the notes, add a new note, and edit/delete existing notes.
    Completed Teacher's Student Notes tab

Implementation of the Security System

The security system in the Ed-Fi Powered Dashboard application is implemented using a combination of claims-based security and ownership-based permissions. Claims-based security is implemented using Windows Identity Foundation, while ownership-based security makes use of dashboard data to determine the relationship users to the data they are attempting to view. The security implementation uses software development techniques such as Inversion of Control and Aspect-Oriented Programming.

Overriding Core Functionality and Views

Claims-based security is used to restrict access to the data users may retrieve based on the claims they have been issued by an Identity Provider. Teachers will generally be issued claims enabling them to see only certain pages and information, while superintendents will typically be allowed to see all data in their district.