Статьи

MasterPages ненавидят логику. Бросай интерфейсы у них.

MasterPages , пожалуй, самая полезная новая функция в ASP.NET 2.0. Они решают одну из самых больших проблем с 1.1 — предоставляя чистый способ реализации общего внешнего вида приложения. На самом деле они настолько ловкие, что в конечном итоге поощряют некоторые плохие практики. А именно, используя MasterPage сайта в качестве класса базовой страницы сайта. Теперь произнесите это три раза: главные страницы предназначены не для логики, а для внешнего вида.

Нельзя сказать, что использование какого-либо класса базовых страниц — плохая идея. На самом деле это очень хорошая идея. Но базовый класс страницы все еще означает класс, который наследуется от System.Web.UI.Page . Задача состоит в том, что теперь, с MasterPages и всем остальным, возникает небольшой затруднение: в конце концов, эта логика должна быть выражена в веб-элементах управления. И MasterPage, а не класс базовой страницы, непосредственно знает об этих элементах управления. Но базовый класс страницы не является, по крайней мере, если только вы не хотите начать грязную игру вызова Master.FindControl (). Тем не менее, я утверждаю, что базовый класс страницы не обязательно должен знать об этих элементах управления, а лишь о некоторых функциях этих элементов управления. Это своего рода контракт, если хотите. В .NET удобно иметь программные контракты, они просто известны как интерфейсы . И они хороши.

Я уже потерял тебя? Хорошо, давайте просто погрузимся в сценарии и примеры кода, а?

Сценарий

Ваш проект требует, чтобы общий набор кнопок отображался на всех страницах. Эти кнопки должны быть Add, Edit, Delete и Exit. Теперь некоторые страницы не позволяют использовать одну или все эти кнопки. А особенности того, что происходит при нажатии этих кнопок, должны обрабатываться каждой конкретной страницей.

Стратегия

Что мы собираемся сделать здесь:

  1. Создайте интерфейс ICommandStrip, который инкапсулирует необходимую функциональность. А именно события для добавления, удаления, редактирования и выхода. А также набор логических значений для включения или отключения команд.
  2. Создайте другой интерфейс, ICommandStripContainer, для реализации содержащего элемента управления. Его единственная цель в жизни — передать ссылку на конкретный ICommandStrip.
  3. Пользовательский элемент управления, который реализует ICommandStrip
  4. Абстрактный базовый класс MasterPage, который реализует ICommandStripContainer и реализует некоторые основные функции.
  5. Конкретный класс MasterPage, который реализует абстрактный класс в # 4.
  6. Базовый класс Page, который предоставляет ссылку на наш ICommandStripContainer и обрабатывает некоторые основные элементы для указанного контейнера.
  7. Реальная страница, которая связывается с командной строкой, чтобы доказать, что все работает плавно.

Итак, во-первых, ICommandStrip:

/// <summary> /// Command strip interface contract. /// </summary> public interface ICommandStrip { /// <summary> /// Fired when the Add command is enacted. /// </summary> event EventHandler AddClicked; /// <summary> /// Fired when the Edit command is enacted. /// </summary> event EventHandler EditClicked; /// <summary> /// Fired when the Delete command is enacted. /// </summary> event EventHandler DeleteClicked; /// <summary> /// Fired when the Exit command is enacted. /// </summary> event EventHandler ExitClicked; /// <summary> /// Is the Add commmand enabled? /// </summary> bool IsAddable { get; set; } /// <summary> /// Is the Delete command enabled? /// </summary> bool IsDeletable { get; set; } /// <summary> /// Is the Edit command enabled? /// </summary> bool IsEditable { get; set; } /// <summary> /// Is the Exit command enabled? /// </summary> bool IsExitable { get; set;} } 

Теперь ICommandStripContainer:

/// <summary> /// Container interface for our Command Strip /// </summary> public interface ICommandStripContainer { /// <summary> /// Gets a reference to the CommandStrip /// </summary> ICommandStrip CommandStrip { get;} }
/// <summary> /// Container interface for our Command Strip /// </summary> public interface ICommandStripContainer { /// <summary> /// Gets a reference to the CommandStrip /// </summary> ICommandStrip CommandStrip { get;} } 

Со мной так далеко? Теперь мы начинаем добираться до хорошей части. Давайте посмотрим на пользовательский элемент управления:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ButtonCommandStrip.ascx.cs" Inherits="ButtonCommandStrip" %> <div style="background-color: #e2e2e2; padding:5px; text-align: right;"> <asp:Button runat="Server" ID="AddButton" OnClick="AddButtonClicked" Text="Add" /> <asp:Button runat="Server" ID="EditButton" OnClick="EditButtonClicked" Text="Edit" /> <asp:Button runat="Server" ID="DeleteButton" OnClick="DeleteButtonClicked" Text="Delete" /> <asp:Button runat="Server" ID="ExitButton" OnClick="ExitButtonClicked" Text="Exit" /> </div>
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ButtonCommandStrip.ascx.cs" Inherits="ButtonCommandStrip" %> <div style="background-color: #e2e2e2; padding:5px; text-align: right;"> <asp:Button runat="Server" ID="AddButton" OnClick="AddButtonClicked" Text="Add" /> <asp:Button runat="Server" ID="EditButton" OnClick="EditButtonClicked" Text="Edit" /> <asp:Button runat="Server" ID="DeleteButton" OnClick="DeleteButtonClicked" Text="Delete" /> <asp:Button runat="Server" ID="ExitButton" OnClick="ExitButtonClicked" Text="Exit" /> </div> 

И код позади:

public partial class ButtonCommandStrip : System.Web.UI.UserControl, ICommandStrip { public ButtonCommandStrip() { PreRender += new EventHandler(bindButtonAbilities); } private void bindButtonAbilities(object sender, EventArgs e) { AddButton.Enabled = IsAddable; EditButton.Enabled = IsEditable; DeleteButton.Enabled = IsDeletable; ExitButton.Enabled = IsExitable; } #region ICommandStrip Members private event EventHandler addClicked; public event EventHandler AddClicked { add { addClicked += value; } remove { addClicked -= value; } } private event EventHandler editClicked; public event EventHandler EditClicked { add { editClicked += value; } remove { editClicked -= value; } } private event EventHandler deleteClicked; public event EventHandler DeleteClicked { add { deleteClicked += value; } remove { deleteClicked -= value; } } private event EventHandler exitClicked; public event EventHandler ExitClicked { add { exitClicked += value; } remove { deleteClicked -= value; } } public bool IsAddable { get { if (ViewState["IsAddable"]==null) { return true; } return (bool)ViewState["IsAddable"]; } set { ViewState["IsAddable"] = value; } } public bool IsDeletable { get { if (ViewState["IsDeletable"] == null) { return true; } return (bool)ViewState["IsDeletable"]; } set { ViewState["IsDeletable"] = value; } } public bool IsEditable { get { if (ViewState["IsEditable"] == null) { return true; } return (bool)ViewState["IsEditable"]; } set { ViewState["IsEditable"] = value; } } public bool IsExitable { get { if (ViewState["IsExitable"] == null) { return true; } return (bool)ViewState["IsExitable"]; } set { ViewState["IsExitable"] = value; } } #endregion protected void AddButtonClicked(object sender, EventArgs e) { if (addClicked != null) { addClicked(this, e); } } protected void EditButtonClicked(object sender, EventArgs e) { if (editClicked != null) { editClicked(this, e); } } protected void DeleteButtonClicked(object sender, EventArgs e) { if (deleteClicked != null) { deleteClicked(this, e); } } protected void ExitButtonClicked(object sender, EventArgs e) { if (exitClicked != null) { exitClicked(this, e); } } }
public partial class ButtonCommandStrip : System.Web.UI.UserControl, ICommandStrip { public ButtonCommandStrip() { PreRender += new EventHandler(bindButtonAbilities); } private void bindButtonAbilities(object sender, EventArgs e) { AddButton.Enabled = IsAddable; EditButton.Enabled = IsEditable; DeleteButton.Enabled = IsDeletable; ExitButton.Enabled = IsExitable; } #region ICommandStrip Members private event EventHandler addClicked; public event EventHandler AddClicked { add { addClicked += value; } remove { addClicked -= value; } } private event EventHandler editClicked; public event EventHandler EditClicked { add { editClicked += value; } remove { editClicked -= value; } } private event EventHandler deleteClicked; public event EventHandler DeleteClicked { add { deleteClicked += value; } remove { deleteClicked -= value; } } private event EventHandler exitClicked; public event EventHandler ExitClicked { add { exitClicked += value; } remove { deleteClicked -= value; } } public bool IsAddable { get { if (ViewState["IsAddable"]==null) { return true; } return (bool)ViewState["IsAddable"]; } set { ViewState["IsAddable"] = value; } } public bool IsDeletable { get { if (ViewState["IsDeletable"] == null) { return true; } return (bool)ViewState["IsDeletable"]; } set { ViewState["IsDeletable"] = value; } } public bool IsEditable { get { if (ViewState["IsEditable"] == null) { return true; } return (bool)ViewState["IsEditable"]; } set { ViewState["IsEditable"] = value; } } public bool IsExitable { get { if (ViewState["IsExitable"] == null) { return true; } return (bool)ViewState["IsExitable"]; } set { ViewState["IsExitable"] = value; } } #endregion protected void AddButtonClicked(object sender, EventArgs e) { if (addClicked != null) { addClicked(this, e); } } protected void EditButtonClicked(object sender, EventArgs e) { if (editClicked != null) { editClicked(this, e); } } protected void DeleteButtonClicked(object sender, EventArgs e) { if (deleteClicked != null) { deleteClicked(this, e); } } protected void ExitButtonClicked(object sender, EventArgs e) { if (exitClicked != null) { exitClicked(this, e); } } } 

Ничего особенного там нет. Он просто реализует ICommandStrip, а также некоторую сантехнику для включения или отключения кнопок в зависимости от ситуации.

Теперь вернемся к App_Code для базового класса главной страницы:

public abstract class SiteMasterPageBase : MasterPage, ICommandStripContainer { /// <summary> /// Internal mapping property to get a reference to the concrete ICommandStrip control /// </summary> protected abstract ICommandStrip CommandStripControl { get; } /// <summary> /// Returns a reference to the CommandStripControl for use by the SitePage. /// </summary> public ICommandStrip CommandStrip { get { return CommandStripControl; } } }
public abstract class SiteMasterPageBase : MasterPage, ICommandStripContainer { /// <summary> /// Internal mapping property to get a reference to the concrete ICommandStrip control /// </summary> protected abstract ICommandStrip CommandStripControl { get; } /// <summary> /// Returns a reference to the CommandStripControl for use by the SitePage. /// </summary> public ICommandStrip CommandStrip { get { return CommandStripControl; } } } 

Единственное, что здесь происходит — это абстрактный класс. Почему ты спрашиваешь? На самом деле довольно просто — у вас может быть несколько главных страниц для сайта. И если вы поддерживаете те же внутренние интерфейсы, вы можете переключаться между ними на лету. Или, по крайней мере, в событии PreInit. Но затем уловка заключается в подключении главных страниц к элементам управления, которыми они должны манипулировать; следовательно, абстрактное свойство CommandStripControl. Его цель — служить мостом между конкретным классом MasterPage и обычным интерфейсом.

Просто чтобы доказать это легко, давайте заглянем на главную страницу нашего сайта:

<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs" Inherits="Site" %> <%@ Register Src="ButtonCommandStrip.ascx" TagName="ButtonCommandStrip" TagPrefix="SpUc" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>SpBlog Test Page</title> </head> <body> <form id="form1" runat="server"> <h1>Master Page Command Strip Demo</h1> <SpUc:ButtonCommandStrip id="CommandStripCtl" runat="server" /> <asp:contentplaceholder id="MainContentPlaceHolder" runat="server" /> </form> </body> </html>
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs" Inherits="Site" %> <%@ Register Src="ButtonCommandStrip.ascx" TagName="ButtonCommandStrip" TagPrefix="SpUc" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>SpBlog Test Page</title> </head> <body> <form id="form1" runat="server"> <h1>Master Page Command Strip Demo</h1> <SpUc:ButtonCommandStrip id="CommandStripCtl" runat="server" /> <asp:contentplaceholder id="MainContentPlaceHolder" runat="server" /> </form> </body> </html> 

И код, который ничего не делает, говорит MasterPage, как найти ICommandStrip:

public partial class Site : SiteMasterPageBase { protected override ICommandStrip CommandStripControl { get { return this.CommandStripCtl; } } }
public partial class Site : SiteMasterPageBase { protected override ICommandStrip CommandStripControl { get { return this.CommandStripCtl; } } } 

Работая рука об руку (или Interface-to-Interface, lol) с этим классом SiteMasterPageBase, это класс SitePageBase:

public class SitePageBase : Page { public SitePageBase() { //Wire up the command strip. //Note that Init is the earliest we can hook up the events. //They are not avaliable before the MasterPage has loaded its controls. Init+=new EventHandler(BindCommandStrip); } /// <summary> /// Gets a reference to the Master CommandStripContainer /// </summary> protected ICommandStripContainer CommandStripContainer { get { return (ICommandStripContainer)Master; } } #region commandstrip utility /// <summary> /// Binds the command strip events /// </summary> /// <param name="sender">sender</param> /// <param name="e">empty event args</param> private void BindCommandStrip(object sender, EventArgs e) { if (CommandStripContainer != null) { CommandStripContainer.CommandStrip.AddClicked += new EventHandler(AddClicked); CommandStripContainer.CommandStrip.DeleteClicked += new EventHandler(DeleteClicked); CommandStripContainer.CommandStrip.EditClicked += new EventHandler(EditClicked); CommandStripContainer.CommandStrip.ExitClicked += new EventHandler(ExitClicked); } } #region virtual event handler stubs protected virtual void ExitClicked(object sender, EventArgs e) {} protected virtual void EditClicked(object sender, EventArgs e) {} protected virtual void DeleteClicked(object sender, EventArgs e) {} protected virtual void AddClicked(object sender, EventArgs e) { } #endregion #endregion }
public class SitePageBase : Page { public SitePageBase() { //Wire up the command strip. //Note that Init is the earliest we can hook up the events. //They are not avaliable before the MasterPage has loaded its controls. Init+=new EventHandler(BindCommandStrip); } /// <summary> /// Gets a reference to the Master CommandStripContainer /// </summary> protected ICommandStripContainer CommandStripContainer { get { return (ICommandStripContainer)Master; } } #region commandstrip utility /// <summary> /// Binds the command strip events /// </summary> /// <param name="sender">sender</param> /// <param name="e">empty event args</param> private void BindCommandStrip(object sender, EventArgs e) { if (CommandStripContainer != null) { CommandStripContainer.CommandStrip.AddClicked += new EventHandler(AddClicked); CommandStripContainer.CommandStrip.DeleteClicked += new EventHandler(DeleteClicked); CommandStripContainer.CommandStrip.EditClicked += new EventHandler(EditClicked); CommandStripContainer.CommandStrip.ExitClicked += new EventHandler(ExitClicked); } } #region virtual event handler stubs protected virtual void ExitClicked(object sender, EventArgs e) {} protected virtual void EditClicked(object sender, EventArgs e) {} protected virtual void DeleteClicked(object sender, EventArgs e) {} protected virtual void AddClicked(object sender, EventArgs e) { } #endregion #endregion } 

Здесь тоже ничего особенного. Некоторый код для связывания событий и некоторые виртуальные методы-заглушки для легкого переопределения позже. Обратите внимание на метод CommandStripContainer — это мост назад к элементу управления ICommandStrip MasterPage.

Теперь хорошая часть — простой пример конкретной реализации. Сначала файл ASPX:

<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="CommandStripPage.aspx.cs" Inherits="CommandStripPage" Title="Untitled Page" %> <asp:Content ID="MainContentPlaceHolder" ContentPlaceHolderID="MainContentPlaceHolder" Runat="Server"> <p>Last Command: <asp:Label runat="Server" ID="WhatWasClicked" Text="Nothing Yet" ForeColor="red" Font-Bold="true" /></p> <asp:CheckBox ID="AddableCheckbox" Text="Addable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="EditableCheckbox" Text="Editable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="DeletableCheckbox" Text="Deletable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="ExitableCheckbox" Text="Exitable" runat="Server" Checked="true" /><br /> <asp:Button runat="Server" ID="ApplyChangesButton" Text="Apply Ability Changes" OnClick="ApplyChanges" /> </asp:Content>
<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="CommandStripPage.aspx.cs" Inherits="CommandStripPage" Title="Untitled Page" %> <asp:Content ID="MainContentPlaceHolder" ContentPlaceHolderID="MainContentPlaceHolder" Runat="Server"> <p>Last Command: <asp:Label runat="Server" ID="WhatWasClicked" Text="Nothing Yet" ForeColor="red" Font-Bold="true" /></p> <asp:CheckBox ID="AddableCheckbox" Text="Addable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="EditableCheckbox" Text="Editable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="DeletableCheckbox" Text="Deletable" runat="Server" Checked="true" /><br /> <asp:CheckBox ID="ExitableCheckbox" Text="Exitable" runat="Server" Checked="true" /><br /> <asp:Button runat="Server" ID="ApplyChangesButton" Text="Apply Ability Changes" OnClick="ApplyChanges" /> </asp:Content> 

А теперь биты C #:

public partial class CommandStripPage : SitePageBase { protected override void EditClicked(object sender, EventArgs e) { WhatWasClicked.Text = "EDIT was clicked!"; } protected override void DeleteClicked(object sender, EventArgs e) { WhatWasClicked.Text = "DELETE was clicked!"; } protected override void AddClicked(object sender, EventArgs e) { WhatWasClicked.Text = "ADD was clicked!"; } protected override void ExitClicked(object sender, EventArgs e) { WhatWasClicked.Text = "EXIT was clicked!"; } protected void ApplyChanges(object sender, EventArgs e) { CommandStripContainer.CommandStrip.IsAddable = AddableCheckbox.Checked; CommandStripContainer.CommandStrip.IsDeletable = DeletableCheckbox.Checked; CommandStripContainer.CommandStrip.IsEditable = EditableCheckbox.Checked; CommandStripContainer.CommandStrip.IsExitable = ExitableCheckbox.Checked; } }
public partial class CommandStripPage : SitePageBase { protected override void EditClicked(object sender, EventArgs e) { WhatWasClicked.Text = "EDIT was clicked!"; } protected override void DeleteClicked(object sender, EventArgs e) { WhatWasClicked.Text = "DELETE was clicked!"; } protected override void AddClicked(object sender, EventArgs e) { WhatWasClicked.Text = "ADD was clicked!"; } protected override void ExitClicked(object sender, EventArgs e) { WhatWasClicked.Text = "EXIT was clicked!"; } protected void ApplyChanges(object sender, EventArgs e) { CommandStripContainer.CommandStrip.IsAddable = AddableCheckbox.Checked; CommandStripContainer.CommandStrip.IsDeletable = DeletableCheckbox.Checked; CommandStripContainer.CommandStrip.IsEditable = EditableCheckbox.Checked; CommandStripContainer.CommandStrip.IsExitable = ExitableCheckbox.Checked; } } 

Как видите, здесь тоже нет ничего особенного. Просто обрабатывать некоторые события, как обычную страницу ASP.NET. Но хитрость заключается в том, что все эти события связаны с SiteMasterPageBase & SitePageBase без особой заботы об этой странице. А когда вам нужно поменять структуры управления, вам нужно будет поменять их только в одном месте, потому что ваши страницы используют только определенные интерфейсы, а не классы.

Наслаждайтесь счастливым кодированием и не забывайте пнуть его, если вам это нравится

[Несколько редакционных заметок и крик: вдохновение для этой демонстрации пришло из этой темы на форумах SitePoint ASP.NET . И я позаимствовал большую часть интерфейса из поста Пуфы . Его перечисление немного более элегантно, чем мои bool, но я стал ленивым.]