Versions Compared

Key

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

Overview


This documentation provides sample implementations of the plugin architecture for Ed-Fi Dashboards. The intent is to provide detailed information about the creation of plugins and the complete interaction with data, services, and views while documenting the overall plugin architecture and major conventions so that an accomplished developer in the technology can successfully create plugins.

Audience

The primary audience for this document are developers implementing plugins for dashboards powered by Ed-Fi.

Prerequisites

The Ed-Fi platform architecture uses a collection of design patterns, architectural best-practices, conventions, and databases. In order to understand the code base and be able to write a plugin, it is important to understand these patterns and techniques. It is recommended that a developer intending to implement a plugin has knowledge and understanding of the dashboard’s extension mechanisms and has completed the dashboard setup guide. Setup information is available in the Ed-Fi Dashboards Getting Started guide and detail about the dashboard architecture can be found in the Ed-Fi Dashboard Developers' Guide.

Basic Guidelines for Implementing a Plugin

There are a few basic guidelines that are very important and should be followed
when followed when developing a plugin:

  1. All plugins should be mutually exclusive and self-contained. Plugins are intended to be new functionality that gets added to the dashboards.
  2. Plugins follow the basic Ed-Fi conventions for extensions. See "Extension Framework Guide.
  3. Plugins should use the convention-based project names starting always with EdFi.Dashboards.Plugins.[PluginName].[ProjectType]. For example: EdFi.Dashboards.Plugins.[PluginName].Resources,
    EdFi.Dashboards.Plugins.[PluginName].Data.
  4. A new plugin should always be developed under the current EdFi.Dashboards.Plugins.Web version. This means that if this project is MVC 3 with .Net Framework 4.0, you should use MVC 3 with .Net Framework 4.0 or the plugin will not work.
  5. JavaScript and CSS files need to be declared as Embedded Resources.

Technical Overview

The plugin architecture hooks into the existing dashboard by leveraging the CastleWindsor Inversion of Control (IoC) container. When the container is created, on Application_Start, it looks in the ~/Plugins
directory for appropriately named DLLs. If it finds any plugins, it then uses reflection to create instances of classes that implement the IWindsorInstaller interface. The IWindsorInstaller classes are responsible for wiring up the dependency injection for the plugin.

Once the IoC container has been initialized, the dashboard initializes the MVC framework. During this initialization, the dashboard will again look in the ~/Plugins directory for appropriately named DLLs. If it finds any plugins, it then registers all of the custom routes, controllers, and views. If the plugin has embedded JavaScript or CSS files, it creates the Cassette bundles for those embedded resources.

At this point, the plugin is available to be consumed. 

Walkthrough

Walk-through 1: Hello World (Sample Plugin)

Objective: The first plugin will be simple. You will create a plugin to show a view at the “Admin Area” that will display the words “Hello World”. The code for this walkthrough walk through can be found on our Ed-Fi-Samples Repository ~\Sample Plugins/EdFi.Dashboards.Plugins.HelloWorld

Creating the Plugin

 

  1. Create a new empty MVC project utilizing the Razor View engine and name it: EdFi.Dashboards.Plugins.HelloWorld.Web

    Image Added
    Image Added

  2. Clean up the project by deleting any default folders and files (App_Start, Content, Global.asax, etc.) with the exception of Properties\AssemblyInfo.cs. You won’t be needing these because the EdFi.Dashboards.Presentation.Web project already provides what is needed.
  3. Add the following references to the project: 
    1. EdFi.Dashboards.Core 
    2. EdFi.Dashboards.Presentation.Core
    3. EdFi.Dashboards.Resources 
    4. EdFi.Dashboards.Resources.Models 
    5. EdFi.Dashboards.Reosource.Models.Common 
  4. Create the following folder structure starting at the root of the Hello World project: “~\Areas\Admin\Controllers\” and “~\Areas\Admin\Views\”.
    Image Added
  5. Create a controller under the “~\Areas\Admin\Controllers\” folder and name it HelloWorldController.

    Code Block
    languagec#
    using System.Web.Mvc; namespace EdFi.Dashboards.Plugins.HelloWorld.Web.Areas.Admin.Controllers { 
    /// <summary> 
    /// Controller for the plugin's view. 
    /// The folder structure uses ASP.NET MVC conventions of Areas. An area is effectively an MVC structure inside an application. 
    /// Your plugin should either reside inside of an existing Area (see EdFi.Dashboards.Presentation.Web) 
    /// or be sure to contain code that registers the area (see EdFi.Dashboards.Presentation.Core) 
    /// </summary>
     public class HelloWorldController : Controller 
     { 
    	/// <summary> 
    	/// Controller constructor
    	/// </summary> 
         public HelloWorldController() 
         {
         /// <summary> 
    	 /// The Get method for the controller. 
    	 /// </summary> 
    	 /// <param name="localEducationAgencyId">Since our plugin resides within the admin area it requires a LEA id as a       parameter.</param> 
    	  public ActionResult Get(int localEducationAgencyId)
          { 
    		return View(); 
    	   } 
    }
  6. If Visual Studio created a default Index action method, replace it with the standard Get action method. It is very important for security reasons to add the localEducationAgencyId to the signature of the Get method. 
  7. Create the corresponding Get view “~\Areas\Admin\Views\HelloWorld\Get.cshtml”. Make sure you set the custom tool property of the view to “RazorGenerator” so it generates a precompiled version of the view that will get embedded in the .dll.
    Image Added
  8. It is very important to wrap the content in the @section ContentPlaceHolder1 { } razor directive because the dashboard’s main layouts and corresponding area layouts expect this.

    Code Block
    languagexml
    @{ 
    // The view for our plugin. This file must have the CustomTool property set to RazorGenerator which is a Visual Studio Extension. 
    // ContentPlaceHolderHead and ContentPlaceHolder1 are view sections defined by the dashboard rendering engine.   
          ViewBag.Title = "Hello World"; 
    }
    @section ContentPlaceHolderHead { 
      <title>Hello World</title> 
    } 
    @section ContentPlaceHolder1 { 
      <h1>Hello World</h1> 
    }
  9. Add a reference to Castle.Windsor. It is recommended that you use the same version of CastleWindsor that the dashboard is using. In the Package Manager Console, type the following command:

    Code Block
    languagepowershell
    PM> Install-Package Castle.Windsor -Version 2.5.1.0
  10. Create a marker interface so that you can reference the current plugin’s assembly: “~\Marker_EdFi_Dashboards_Plugins_HelloWorld_Web.cs”Create a “~\Utilities\CastleWindsor\Installer.cs” file in the specified path. This is needed to tell the IoC container to register the plugin’s web artifacts like controllers. There are 4 default installers provided in EdFi.

     

    Code Block
    languagec#
    namespace EdFi.Dashboards.Plugins.HelloWorld.Web 
    {
    	/// <summary> 
    /// Marker interface - Needed for the CastleWindsor installer 
    /// </summary> 
    public interface Marker_EdFi_Dashboards_Plugins_HelloWorld_Web 
    { 
    } 
    }

Create a “~\Utilities\CastleWindsor\Installer.cs” file in the specified path. This is needed to tell the IoC container to register the plugin’s web artifacts like controllers. There are 4 default installers provided in EdFi.Dashboards.Presentation.Core to inherit from. You will be using

the WebDefaultConvetionInstaller for this plugin.

 

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;  

the WebDefaultConvetionInstaller for this plugin.

 

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.HelloWorld.Web.Utilities.CastleWindsor 
{ 
	/// <summary> 
	/// Installer class for this plugin.
	/// This is needed to resolve the CastleWindsor IoC Container and hook in this plugin with the main site. 
	/// </summary> 
	public class Installer : WebDefaultConventionInstaller<Marker_EdFi_Dashboards_Plugins_HelloWorld_Web> 
	{ 
		public override void Install(IWindsorContainer container, IConfigurationStore store) 
		{ 
			base.Install(container, store);
			//This registers our PluginManifest class as able to fullfill the IPluginManifest interface with the 	CastleWindsor IoC 
			//The IPluginManifest is what will add a new menu(tab) to an existing section. 
			container.Register(Component 
.For<IPluginManifest>() 
.ImplementedBy<PluginManifest>()); 
		} 
	} 
}
Info

NOTE: A common mistake when creating a view is to forget to set the custom tool on the view to be RazorGenerator. This tool will generate a .cs version of the view that will be compiled and embedded into the .dll.

Integrating the Plugin into the Menu

To make the plugin show up in the menu you have to implement the IPluginManifest interface. The plugin manifest contains the following 3 properties:

  • The Name of the plugin
  • The Version
  • A collection of the Menu Items that can be added to the different areas that the dashboard has to implement the IPluginManifest interface:
  1. In the root of your plugin project, add a PluginManifest class that implements IPluginManifest.

    Code Block
    languagec#
    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.Plugins;
    using System.Collections.Generic;
     
    namespace EdFi.Dashboards.Plugins.HelloWorld.Web 
    { 
    	/// <summary> 
    	/// Manifest class for this plugin. 
    	/// </summary> 
    	public class PluginManifest : IPluginManifest
  2. In the constructor of PluginManifest pass in an IAdminAreaLinks object and assign it to a private member variable. The IAdminAreaLinks represent the existing menus that you will be extending.

    Code Block
    languagec#
    private readonly IAdminAreaLinks _adminAreaLinks;
    	/// <summary>
    	/// Constructor 
    	/// </summary> 
    	/// <param name="adminAreaLinks">Since our plugin is under the Admin area it requires the AdminAreaLinks to be able to add a new menu.</param> 
    	public PluginManifest(IAdminAreaLinks adminAreaLinks) 
    	{ 
    		this._adminAreaLinks = adminAreaLinks; 
    	}
  3. Implement the Name and Version properties of the class. It is recommended that you use reflection to return the Name and Version of the assembly represented in the Properties\AssemblyInfo.cs file.

    Code Block
    languagec#
    /// <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(); } 
    }
  4. Implement the IEnumerable<PluginMenu> property. Here you make sure that context has a valid LocalEducationAgencyId. Then you assign your PluginMenu to the Admin area and provide the text and URL to navigate to when the menu is clicked. Using the built-in helper method “Resource” of the IAdminAreaLinks and passing in the string “HelloWorld” will resolve to ~Admin/HelloWorld.

    Code Block
    languagec#
    /// <summary> 
    /// Menu definition for the plugin
    /// </summary>
    public IEnumerable<PluginMenu> PluginMenus 
    { 
    	get { return GetPluginMenus(); } 
    } 
    private IEnumerable<PluginMenu> GetPluginMenus() 
    { 
    	var requestContext = EdFiDashboardContext.Current; 
    	if (requestContext.LocalEducationAgencyId.HasValue) 
    	{ 
    		return new List<PluginMenu> 
    		{ 
    			new PluginMenu{ 
    			//This corresponds to the Area where the menu is too be added
    			Area = "Admin",
    			ResourceModels = new List<ResourceModel> 
    			{ 
    				new ResourceModel 
    				{ 
    					// the text to display on the menu
    					Text = "Sample Plugin", 
    					// the URL to resolve when clickin on the menu.
    					// the link.Resource() registration method is a helper method provided to easily construct the proper url (~/Admin/HelloWorld)
    					Url = _adminAreaLinks.Resource(requestContext.LocalEducationAgencyId.Value, "HelloWorld"),
    				}
    			} 
    		} 
    	}; 
    } return new List<PluginMenu>();} 
    }}
  5. Create a “~\Utilities\CastleWindsor\Installer.cs” file in the specified path. This is needed to tell the IoC container to register the plugin’s web artifacts like controllers. There are 4 default installers provided in EdFi.Dashboards.Presentation.Core to inherit from. You will be using the WebDefaultConvetionInstaller for this plugin. 

  6. Register the plugin manifest implementation in the Installer located in “~/Utilities/CastleWindsor/Installer.cs”. The figure below shows the code for registering the plugin manifest implementation.

    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.HelloWorld.Web.Utilities.CastleWindsor 
    {
    	/// <summary> 
    	/// Installer class for this plugin. 
    	/// This is needed to resolve the CastleWindsor IoC Container and hook in this plugin with the main site. 
    	/// </summary> 
    	public class Installer : WebDefaultConventionInstaller<Marker_EdFi_Dashboards_Plugins_HelloWorld_Web> 
    	{
    		public override void Install(IWindsorContainer container, IConfigurationStore store) 
    		{ 
    			base.Install(container, store); 
    			//This registers our PluginManifest class as able to fullfill the IPluginManifest interface with the CastleWindsor IoC 
    			//The IPluginManifest is what will add a new menu(tab) to an existing section. 
    			container.Register(Component 
    				.For<IPluginManifest>() 
    				.ImplementedBy<PluginManifest>()); 
    		} 
    	} 
    }
  7. Build the solution. 

  8. Copy the compiled assembly and .pdb file to the Plugins folder for the dashboard and perform an iisreset, if necessary.

  9. Navigate to the dashboard and login as and admin. You can use KurtMcCarthy with anything as the password except an empty string or the text “wrong-password.” This is an account that maps to an IT Administrator for Grand Bend ISD on the sample dataset provided. Then replace the route with “~Admin/HelloWorld” and your plugin view should show Hello World on your browser.

Image Added

Walk-through 2: Teacher’s Student Notes (CRUD Example)

Objective: Now that you know the basics of how to develop a plugin for the Ed-Fi dashboards, you can take it all the way and implement a full-featured plugin. You will now create a plugin that will let teachers take notes for a given student. It will be an example that will cover everything from creating a persistent storage, form validation, server-side validation, custom plugin services, resources, and custom route registration.

This 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 given note.

The code for this walk through can be found on our Ed-Fi-Samples Repository Sample Plugins/TeacherNotes

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. Just like the Hello World example, create a MVC web application and name it EdFi.Dashboards.Plugins.TeacherNotes.Web.

  1. Attach this plugin at the student school level. Create the following path: “\Areas\StudentSchool\Controllers” and “\Areas\StudentSchool\Views\Notes”.
  2. Create the NotesController in the Controllers folder with the standard Get action result method. Because of 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. Do not forget to add the RazorGenerator tool so it can create a compiled version of the view.

    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.HelloWorld.Web.Utilities.CastleWindsor TeacherNotes.Web 
    { 
    	/// <summary> 
    	/// InstallerMarker classinterface for this plugin.
    	/// This is needed to resolve the CastleWindsor IoC Container and hook in this plugin with the main sitethe EdFi.Dashboards.Plugins.TeacherNotes.Web assembly. 
    	/// </summary> 
    	public class Installer : WebDefaultConventionInstaller<Markerinterface Marker_EdFi_Dashboards_Plugins_HelloWorldTeacherNotes_Web> Web 	public override void Install(IWindsorContainer container, IConfigurationStore store) 
    		{ 
    			base.Install(container, store);
    			//This registers our PluginManifest class as able to fullfill the IPluginManifest interface with the 	CastleWindsor IoC 
    			//The IPluginManifest is what will add a new menu(tab) to an existing section. } 
    }
  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(ComponentComponent 
    				.For<IPluginManifest>() 
    				.ImplementedBy<PluginManifest>()); 
    		} 
    	} 
    }
    Info
    NOTE: A common mistake when creating a view is to forget to set the custom tool on the view to be RazorGenerator. This tool will generate a .cs version of the view that will be compiled and embedded into the .dll.
    }
    }
  8. Compile the code and run the project. Once you navigate down to a student level, you should see the following: the menu integration, the route, and working view.
    Image Added

Include Page
Developers' Guide
Developers' Guide