ASP.NET architektura dla formularzy na przykładzie Webpart’a i okna dialogowego Sharepoint
Opublikowano: 2019-08-23 , wyświetlono: 11250
Po kilku projektach stworzonych na platformę Sharepoint zauważyłem pewien problem podczas tworzenia formularzy. Niekiedy prosty błąd powodował komunikat z identyfikatorem korelacji, co skutkowało przeglądaniem log’a serwera co nie jest przyjemne i ponownym kompilowaniem i wdrażaniem rozwiązania. To wszystko zajmowało czas, a bywało i tak, że miałem możliwość pracowania tylko na serwerze produkcyjnym co w istotny sposób ograniczało mi możliwości takich operacji. Pomyślałem, że dobrze by było znaleźć jakiś sposób by prostą testową aplikacją konsolową sprawdzać zachowanie się formularzy. Chodziło mi o to, czy dane początkowe poprawnie się wczytały, czy operacja zapisu jest realizowana poprawnie, itp.
Przypomniałem sobie, że kiedyś dla aplikacji desktopowych stosowałem dla formularzy taką prostą architekturę model-view-presenter. Postanowiłem zaadaptować ją jakoś do potrzeb do środowiska ASP.NET.
Pracę rozpocząłem od stworzenia abstrakcyjnej klasy prezentera i jego interfejsu, by z nich dziedziczyły konkretne formularze.W prezenterze BasePresenter umieściłem obiekt widoku _view, obiekt odpowiadający za transakcje biznesowe w aplikacji _api, obiekt służący do komunikacji z bazą danych lub listami sharepoint _dal, konfigurację aplikacji _config i kilka metod do wyświetlania komunikatów w formularzach. Do tego dodałem niewielki interfejs IBaseView.
IBaseView.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ChinasoftServiceCore.Presenter { public interface IBaseView { string ConfigSiteUrl { get; set; } string ConfigListName { get; set; } string Message { get; set; } } }
BasePresenter.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using ChinasoftServiceCore.Domain; using ChinasoftServiceCore.DataAccess; namespace ChinasoftServiceCore.Presenter { public abstract class BasePresenter<V> where V : IBaseView { protected V _view; protected Transaction _api; protected DalService _dal; protected IAppConfiguration _config; public BasePresenter(V view) { if (view == null) { throw new ArgumentNullException("View cannot be null."); } _view = view; init(); } public void Message(string s) { _view.Message = String.Format("{0} <br />", s); } public void MessageAppend(string s) { _view.Message += String.Format("{0} <br />", s); } public void MessageAppend(Exception e) { _view.Message += String.Format("Error: {0} <br />", e.Message); } public void MessageError(Exception e) { _view.Message = String.Format("Error: {0} <br />", e.Message); } public void MessageError(string s, Exception e) { _view.Message = String.Format("{0} # Error: {1} <br />", s, e.Message); } private void init() { try { _config = new AppConfiguration(_view.ConfigSiteUrl, _view.ConfigListName); _api = new Transaction(_config); _dal = new DalService(_config); } catch { } } } }
Teraz pokażę implementację prezentera, który odpowiada za wyświetlenie listy obiektów, w tym przypadku są to zgłoszenia. W pierwszym kroku tworzę interfejs. Ten zawiera tylko dla elementy: listę przechowującą obiekty do wyświetlenia i string, który przechowują ta listę w formacie JSON. Format ten potrzebny jest do użytego w formularzu komponentu javascript DataTable
IRequestListView.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint; using ChinasoftServiceCore.Domain; namespace ChinasoftServiceCore.Presenter.FormRequestList { public interface IRequestListView : IBaseView { List<Request> Submissions { get; set; } string JsRequestArray { get; set; } } }
Drugi krok to właściwy prezenter. Jak widać w konstruktorze przekazujemy widok, który ma być obsługiwany przez klasę prezentera. Klasa wczytuje listę zgłoszeń przy swojej inicjalizacji, buduje string dla komponentu tabeli i przekazuje go do widoku.
RequestListPresenter.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint; using ChinaSoftLibrary.Util; using ChinasoftServiceCore.Domain; namespace ChinasoftServiceCore.Presenter.FormRequestList { public class RequestListPresenter : BasePresenter<IRequestListView> { private List<Request> _requests; public RequestListPresenter(IRequestListView view) : base (view) { init(); } public void InitView(bool isPostBack) { if (!isPostBack) { _view.JsRequestArray = toJsArray(); } } private void init() { _api.SetCurrentUser(Sharepoint.GetCurrentUser()); _requests = _dal.LoadRequests(); } /// <summary> /// convert list to Javascript array variable /// </summary> /// <returns></returns> private string toJsArray() { StringBuilder sb = new StringBuilder(); sb.AppendLine("["); foreach (Request r in _requests) { sb.AppendFormat("[ \"{0}\", \"{1}\", \"{2}\", \"{3}\", \"{4}\", \"{5}\", \"{6}\" ],", r.Id, r.RegisteredDate.ToShortDateString(), String.Format("<a class='editLink' href='{0}?RequestID={1}'>{2}</a>", _config.GetRequestEditFormUrl(), r.Id, Function.ConvToJsVar(r.Title)), r.Customer, r.ServicemanCode, r.System, r.Status.Name ); sb.AppendLine(); } if (sb.Length > 3) sb.Length = sb.Length - 3; sb.AppendLine("]"); return (sb.ToString()); } } }
Tak skonstruowany kod powoduje, że mamy testowalnego prezentera. Wystarczy tylko rzeczywisty widok zastąpić klasą testową MockListView i we właściwościach zamieścimy sobie np. kod wyświetlający obiekty, które zostały przekazane z prezentera do widoku. To zwalnia z konieczności wdrażania rozwiązania by przekonać się, czy formularz zostaje prawidłowo zasilony danymi.
public class MockListView : IRequestListView { List<Request> Submissions { get { return (new List<Request>(); }; set { foreach(Request r in value) { Console.WriteLine(r.Id); } } } string JsRequestArray { get; set { Console.WriteLine(value); } } string ConfigSiteUrl { get; set; } string ConfigListName { get; set; } string Message { get; set; } }
Dodatkowym plusem zastosowania takiej architektury i przeniesieniem całego kodu odpowiadającego za logikę zachowania aplikacji do prezentera jest znaczne skrócenie klasy kontrolki ASCX lub ASPX. W przypadku bardziej skomplikowanego formularza sam miałem w przeszłości problem z „puchnącą” klasą formularza i brakiem jej czytelności.
Oczywiście za pomocą takiego prezenter możemy obsłużyć wiele formularzy, różniących się np. prezentacją danych.
Minusem może być skomplikowanie struktury klas, co przy prostych formularzach może wydać się niezbyt celowe.
W moim przypadku lista wyświetlana była w WebPart’cie, które parametrami była adres url site’a z aplikacją i nazwa listy konfiguracji. W związku z tym do projektu dodałem Visual WebPart i dopisałem do niego implementację interfejsu IRequestListView, Skorzystałem z możliwości dzielenia klas na wiele plików źródłowych i całą implementację interfejsu umieściłem w oddzielnym pliku.
Poniżej pełny kod webpart’a. jak ktoś się dokładnie przyjrzy to zauważy, że zbędna jest właściwość Submissions. Została z pierwszej wersji, gdy lista była generowana w oparciu o komponent ASPX ListView
RequestList.cs using System; using System.ComponentModel; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; namespace SpChinasoftService.RequestList { [ToolboxItemAttribute(false)] public class RequestList : WebPart { [DefaultValue("")] [WebDisplayName("Site url")] [Personalizable(PersonalizationScope.Shared)] [WebBrowsable(true)] public string SosSite { get; set; } [DefaultValue("")] [WebDisplayName("Configuration list")] [Personalizable(PersonalizationScope.Shared)] [WebBrowsable(true)] public string SosConfig { get; set; } // Visual Studio might automatically update this path when you change the Visual Web Part project item. private const string _ascxPath = @"~/_CONTROLTEMPLATES/15/SpChinasoftService/RequestList/RequestListUserControl.ascx"; protected override void CreateChildControls() { Control control = Page.LoadControl(_ascxPath); RequestListUserControl c = (RequestListUserControl)control; c.ConfigSiteUrl = this.SosSite; c.ConfigListName = this.SosConfig; Controls.Add(c); } } } RequestListUserControl.ascx.view.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using ChinaSoftLibrary.Util; using ChinasoftServiceCore.Domain; using ChinasoftServiceCore.Presenter.FormRequestList; namespace SpChinasoftService.RequestList { public partial class RequestListUserControl : UserControl, IRequestListView { private string _configSiteUrl; private string _configListName; public string JsRequestArray { get; set; } public List<Request> Submissions { get { return (new List<Request>()); } set { setRequests(value); } } public string ConfigSiteUrl { get { return (_configSiteUrl); } set { _configSiteUrl = value; } } public string ConfigListName { get { return (_configListName); } set { _configListName = value; } } public string Message { get { return (LiteralMessage.Text); } set { LiteralMessage.Text = value; } } private void setRequests(List<Request> items) { //ListViewTask.DataSource = items; //ListViewTask.DataBind(); } } } RequestListUserControl.ascx.cs using System; using System.Collections.Generic; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using ChinasoftServiceCore.Domain; using ChinasoftServiceCore.Presenter.FormRequestList; namespace SpChinasoftService.RequestList { public partial class RequestListUserControl : UserControl { private RequestListPresenter _presenter; protected void Page_Load(object sender, EventArgs e) { try { _presenter = new RequestListPresenter(this); _presenter.InitView(Page.IsPostBack); } catch (Exception ex) { _presenter.MessageAppend(ex); } } } }
RequestListUserControl.ascx <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> <%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="RequestListUserControl.ascx.cs" Inherits="SpChinasoftService.RequestList.RequestListUserControl" %> <%@ Register Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI.WebControls" TagPrefix="asp" %> <SharePoint:CssRegistration runat="server" ID="CssApp" Name="/_layouts/15/SpChinasoftService/app.css"></SharePoint:CssRegistration> <SharePoint:CssRegistration runat="server" ID="CssCore" EnableCssTheming="true" Name="/_layouts/15/1033/styles/Themable/corev15.css?rev=1Z9jaA8PrXPArbKhAptvIg%3D%3DTAG0"></SharePoint:CssRegistration> <script type="text/javascript" src="/_layouts/15/SpChinasoftService/jquery-1.11.2.min.js"></script> <SharePoint:CssRegistration runat="server" ID="CssDatatable2013" Name="/_layouts/15/SpChinasoftService/datatables.min.css" ></SharePoint:CssRegistration> <script type="text/javascript" charset="utf8" src="/_layouts/15/SpChinasoftService/datatables.min.js"></script> <div id="app"> <h1 class="title">Lista zleceń serwisowych</h1> <br /> <table id="request-list" class="display" width="100%"></table> <br /> </div> <div id="errorMessage"><asp:Literal ID="LiteralMessage" runat="server" /></div> <div id="appFooter"> RequestList WebPart (c) 2019 ChinaSoft </div> <!-- javascript table --> <script type="text/javascript"> var dataSet = <%= JsRequestArray %> ; $(document).ready(function () { $('#request-list').DataTable({ language: { "processing": "Przetwarzanie...", "search": "Szukaj:", "lengthMenu": "Pokaż _MENU_ pozycji", "info": "Pozycje od _START_ do _END_ z _TOTAL_ łącznie", "infoEmpty": "Pozycji 0 z 0 dostępnych", "infoFiltered": "(filtrowanie spośród _MAX_ dostępnych pozycji)", "infoPostFix": "", "loadingRecords": "Wczytywanie...", "zeroRecords": "Nie znaleziono pasujących pozycji", "emptyTable": "Brak danych", "paginate": { "first": "Pierwsza", "previous": "Poprzednia", "next": "Następna", "last": "Ostatnia" }, "aria": { "sortAscending": ": aktywuj, by posortować kolumnę rosnąco", "sortDescending": ": aktywuj, by posortować kolumnę malejąco" } }, pageLength: 25, data: dataSet, columnDefs: [ { name: "ID", targets: 0, title: "ID" }, { name: "Data", targets: 1, title: "Data" }, { name: "Tytuł", targets: 2, title: "Tytuł" }, { name: "Klient", targets: 3, title: "Klient" }, { name: "Serwisant", targets: 4, title: "Serwisant" }, { name: "System", targets: 5, title: "System" }, { name: "Status", targets: 6, title: "Status" } ] }); }); </script>
Podsumowanie
Przedstawione przeze mnie rozwiązanie nie jest może jakąś zaawansowaną architekturą, ale w praktyce dobrze się sprawdza i może być podstawą do tworzenia bardziej skomplikowanych rozwiązań.
I na koniec już jeszcze kod dla formularza okna dialogowego (strona aspx) obsługującego dodawanie zadania do zlecenia. W tym przykładzie prezenter i interfejs widoku jest bardziej rozbudowany.
IDialogTaskView.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint; using ChinasoftServiceCore.Domain; namespace ChinasoftServiceCore.Presenter.DialogTask { public interface IDialogTaskView : IBaseView { int RequestID { get; set; } int TaskID { get; set; } RequestScheduleTask Task { get; set; } void SetServicemans(List<Serviceman> servicemans); int GetParamRequestID(); int GetParamTaskID(); } }
DialogScheduleTask.aspx.view.cs using System; using System.Collections.Generic; using System.IO; using System.Web.UI.WebControls; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; using ChinaSoftLibrary.Util; using ChinasoftServiceCore.Domain; using ChinasoftServiceCore.DataAccess; using ChinasoftServiceCore.Presenter.DialogTask; namespace SpChinasoftService.Dialogs { public partial class DialogScheduleTask : LayoutsPageBase, IDialogTaskView { public int RequestID { get { return (Function.GetInt32(LiteralRequestID.Text)); } set { LiteralRequestID.Text = value.ToString(); } } public int TaskID { get { return (Function.GetInt32(LiteralTaskID.Text)); } set { LiteralTaskID.Text = value.ToString(); } } public RequestScheduleTask Task { get { return (getTask()); } set { setTask(value); } } public void SetServicemans(List<Serviceman> servicemans) { DdlServiceman.Items.Clear(); foreach (Serviceman serv in servicemans) DdlServiceman.Items.Add(new ListItem(serv.Title, serv.Code)); } public int GetParamRequestID() { return (Function.GetInt32(Request.Params["RequestID"])); } public int GetParamTaskID() { return (Function.GetInt32(Request.Params["TaskID"])); } private void setTask(RequestScheduleTask task) { DtcStartDate.SelectedDate = task.Start; DtcStopDate.SelectedDate = task.Stop; TextContact.Text = task.Contact; TextComment.Text = task.Comment; RbVisitType.SelectedValue = task.VisitType; DdlServiceman.SelectedValue = task.ServCode; } private RequestScheduleTask getTask() { RequestScheduleTask task = new RequestScheduleTask(); task.Start = DtcStartDate.SelectedDate; task.Stop = DtcStopDate.SelectedDate; task.Contact = TextContact.Text; task.Comment = TextComment.Text; task.VisitType = RbVisitType.SelectedValue; task.ServCode = DdlServiceman.SelectedValue; return (task); } #region IBaseView implementation public string ConfigSiteUrl { get { return (Request.Params["ConfigSite"].ToString()); } set { } } public string ConfigListName { get { return (Request.Params["ConfigList"].ToString()); } set { } } public string Message { get { return (LiteralMessage.Text); } set { LiteralMessage.Text = value; } } #endregion } } DialogScheduleTask.aspx.cs using System; using System.Collections.Generic; using System.IO; using System.Web; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; using ChinaSoftLibrary.Util; using ChinasoftServiceCore.Domain; using ChinasoftServiceCore.DataAccess; using ChinasoftServiceCore.Presenter.DialogTask; namespace SpChinasoftService.Dialogs { public partial class DialogScheduleTask : LayoutsPageBase, IDialogTaskView { private DialogTaskPresenter _presenter; protected void Page_Load(object sender, EventArgs e) { DtcStartDate.DatePickerFrameUrl = this.ConfigSiteUrl + @"/_layouts/15/iframe.aspx"; DtcStopDate.DatePickerFrameUrl = this.ConfigSiteUrl + @"/_layouts/15/iframe.aspx"; try { _presenter = new DialogTaskPresenter(this); _presenter.InitView(Page.IsPostBack); } catch (Exception ex) { _presenter.MessageError(ex); } } protected void ButtonCancel_Click(object sender, EventArgs e) { closeDialog(); } protected void ButtonSave_Click(object sender, EventArgs e) { if (_presenter.Save()) closeDialog(); else { _presenter.MessageAppend("Wystąpił bład podczas zmiany terminów !"); _presenter.MessageAppend(AppLog.GetHtml()); } } private void closeDialog() { HttpContext context = HttpContext.Current; if (HttpContext.Current.Request.QueryString["IsDlg"] != null) { context.Response.Write("<script type='text/javascript'>window.frameElement.commitPopup()</script>"); context.Response.Flush(); context.Response.End(); } } } }
<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DialogScheduleTask.aspx.cs" Inherits="SpChinasoftService.Dialogs.DialogScheduleTask" DynamicMasterPageFile="~masterurl/default.master" %> <asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server"> </asp:Content> <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server"> <SharePoint:CssRegistration runat="server" ID="CssApp" Name="/_layouts/15/SpChinasoftService/app.css"></SharePoint:CssRegistration> <SharePoint:CssRegistration runat="server" ID="CssCore" EnableCssTheming="true" Name="/_layouts/15/1033/styles/Themable/corev15.css?rev=1Z9jaA8PrXPArbKhAptvIg%3D%3DTAG0"></SharePoint:CssRegistration> <div id="app"> <hr class="ms-rteElement-Hr" /> <asp:Literal ID="LiteralRequestID" runat="server" Visible="false" /> <asp:Literal ID="LiteralTaskID" runat="server" Visible="false" /> <asp:Literal ID="LiteralDlgMode" runat="server" Visible="false" /> <table class="ms-formtable" border="0" cellpadding="0" cellspacing="0" width="100%"> <tr> <td class="ms-formlabel">Rodzaj wizyty:</td> <td class="ms-formbody"> <asp:RadioButtonList ID="RbVisitType" runat="server" RepeatDirection="Horizontal"> <asp:ListItem Value="planowany">planowany</asp:ListItem> <asp:ListItem Value="uzgodniony">uzgodniony</asp:ListItem> <asp:ListItem Value="rzeczywisty">rzeczywisty</asp:ListItem> </asp:RadioButtonList> </td> </tr> <tr> <td class="ms-formlabel">Termin od:</td> <td class="ms-formbody"> <SharePoint:DateTimeControl runat="server" ID="DtcStartDate" /> </td> </tr> <tr> <td class="ms-formlabel">Termin do:</td> <td class="ms-formbody"> <SharePoint:DateTimeControl runat="server" ID="DtcStopDate" /> </td> </tr> <tr> <td class="ms-formlabel">Serwisant:</td> <td class="ms-formbody"> <asp:DropDownList ID="DdlServiceman" runat="server" /> </td> </tr> <tr> <td class="ms-formlabel">Kontakt po stronie klienta:</td> <td class="ms-formbody"> <asp:TextBox ID="TextContact" runat="server" Width="600px" /> </td> </tr> <tr> <td class="ms-formlabel">Komentarz:</td> <td class="ms-formbody"> <SharePoint:InputFormTextBox ID="TextComment" runat="server" TextMode="MultiLine" Rows="4" Columns="80" /> </td> </tr> <tr> <td valign="top"class="ms-formlabel"> </td> <td valign="top" class="ms-formbody"> <asp:Button ID="ButtonSave" runat="server" Text="Zapisz" OnClick="ButtonSave_Click" CssClass="ms-ButtonHeightWidth"></asp:Button> <asp:Button ID="ButtonCancel" runat="server" Text="Anuluj" OnClick="ButtonCancel_Click" CssClass="ms-ButtonHeightWidth"></asp:Button> </td> </tr> </table> </div> <div id="errorMessage"><asp:Literal ID="LiteralMessage" runat="server" /></div> </asp:Content> <asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server"> Application Page </asp:Content> <asp:Content ID="PageTitleInTitleArea" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server" > Application Page </asp:Content>