Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Include Page
_Previous Version Note
_Previous Version Note

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 for 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 pathpaths: \Areas\StudentSchool\Controllers” Controllers and \Areas\StudentSchool\Views\Notes”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".

    Code Block
    languagec#
    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 View. Do not forget to add the RazorGenerator tool so it can create a compiled version of the viewView.

    Code Block
    languagexml
    @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.

    Code Block
    languagec#
    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.

    Code Block
    languagec#
    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.

    Code Block
    languagec#
    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.

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

      Code Block
      languagesql
      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.

      Code Block
      languagesql
      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, so it can  to consume the data structures.

  3. Add references to dependencies: “SystemSystem.Data.Linq”Linq, “EdFiEdFi.Dashboards.Presentation.Core”Core, “Subsonic” Subsonic, and “CastleCastle.Windsor”Windsor

  4. Copy the “Entities” Entities folder, including its content and the app.config from the “EdFiEdFi.Dashboards.Extensions.Data” 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.

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

      Code Block
      languagec#
      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.

    Code Block
    languagec#
    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 “UtilitiesUtilities\CastleWindsor\.

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

Code Block
languagec#
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:

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

To create the custom route:

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

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

Code Block
languagec#
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 , there is an abstraction around generating links or 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.

    Code Block
    languagec#
    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.Web project, under the Utilities\CastleWindsor folder, create a ConfigurationSpecificInstaller class to register previous RouteMappingPreparer and IStudentSchoolAreaLinks implementations.
Code Block
languagec#
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 “EdFiEdFi.Dashboards.Plugins.TeacherNotes.Resources.Models” projectsModels project.
  2. Add a StudentSchool folder and add the “~~\StudentSchool\NoteModel.cs” 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.

    Code Block
    languagec#
    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.

    Code Block
    languagec#
    [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.

      Code Block
      languagec#
      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.

      Code Block
      languagec#
      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.

      Info

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

      Code Block
      languagec#
      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.

      Code Block
      languagec#
      [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.

      Code Block
      languagec#
      [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.

      Code Block
      languagec#
      [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.

      Code Block
      languagec#
      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.

Info

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.

    Code Block
    languagexml
    @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.

    Code Block
    languagexml
    <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.

    Code Block
    languagexml
    @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 View’s client-side behavior. Again you are using jQuery.ajax to handle the delete action.

    Code Block
    languagejs
    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.
Code Block
languagexml
@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 themthe 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.

    Code Block
    languagec#
    [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.

    Code Block
    languagexml
    .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 Web project, add a class for the CassetteConfigurations that inherits from CassetteDefaultConventionBundleConfiguration and uses the marker interface as the type.

    Code Block
    languagec#
    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.

Include Page
_Developers' Guide - Sidebar
_Developers' Guide - Sidebar