65.9K
CodeProject 正在变化。 阅读更多。
Home

C# MVC 中的多用户/资源 Web 日记,支持重复事件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (11投票s)

2016年9月3日

CPOL

16分钟阅读

viewsIcon

48436

downloadIcon

2035

为 .NET MVC 中的 Full Calendar 添加多用户和资源功能

引言

我之前写过一篇 文章,介绍了如何使用开源的 JQuery 插件 'Full Calendar' 在 .NET MVC 中创建日记。那篇文章涵盖了插件使用的基础知识,并演示了约会日记系统通常需要的前后端功能。这包括创建约会/事件、编辑、显示不同视图等。文章中的内容仍然有效且有用。本文在上一篇文章的基础上,展示了如何使用一些新功能来为您的日记/约会/日历应用提供多用户、多资源日历/日记以及重复/重复事件/约会功能。我在文章中附加了一个 MVC 项目,演示了本文讨论的概念 - 下载它以查看正在运行的解决方案。

以下是展示我们将要构建内容的图片

背景

当我们管理自己的约会和日记条目时,我们通常只为自己管理,这没关系。然而,当我们开始围绕其他人安排我们的生活和时间时,我们就需要考虑他们的日程安排以及我们自己的。这对于需要管理多个个人(例如医生、机械师、培训师)时间,以及需要管理资源和设备(例如会议室、便携式/共享办公设备)的组织尤其重要。Full Calendar 2 版本引入了一个新的附加组件,用于通过一种称为Scheduler 的分组视图来显示事件和资源。

此附加组件采用多重许可证,允许用户在 GPL 下免费使用,通过 Creative Commons 获得有限许可,也可以通过商业版本获得。坦率地说,对于所提供的价值而言,商业许可证的费用非常合理。我尝试并使用了大多数可用的商业日历系统,现在每次都是我的首选。它轻量级、快速,而且我认为它比市场上任何其他产品都更灵活。

本文将在上一篇文章的基础上,演示为用户提供功能非常强大的多用户、多资源日历/日记解决方案所需的基础知识。

FullCalendar 资源

概述

在个人日记中,例如 Outlook 日记或 Google 日历,默认情况下,我们看到的是我们自己的个人日程。在多用户环境中,我们需要同时查看和管理许多用户的日程。对于个人日记,我们只能在一个地方,但在多用户情况下,用户可以被视为以不同的方式分组。在 FullCalendar 中,这些分组被称为“Resources”。

我们可能如何对用户进行分组的一些示例

  • 按办公室或地点
  • 按部门
  • 按项目团队
  • 按状态

除了按某个总体主题进行分组外,我们还可能发现用户共享一组事物。我们可能希望单独查看这些内容,或作为一个组进行查看。以下是一些用户可能共享的事物的示例

  • 设备
  • 会议室
  • 远程支持登录帐户

FullCalander 中,显示为分组的项目称为“Resources”。下图显示了它们如何在水平和垂直视图中显示。

视图 1

视图 2

如果我们进一步考虑,我们可以认为一个用户或他们共享的物品可能涉及某种关系,例如某些会议室受到特定办公室的约束,或者用户或他们的设备可能仅在特定地点可用。以下是一些关于这些事物可能如何分组的示例

  • 办公室有许多会议室
  • 用户仅在特定地点可用

为了实现上述灵活性,即我们可以拥有“资源内的资源”,FullCalander 的方法是允许在资源分组之间创建父子关系。请注意,我们不限于拥有从一个资源节点到下一个节点的相同项目或关系 - 如果我们想拥有一个没有子节点的顶级资源节点,下一个有三个,下一个有五个,下一个有八个,以及其中每个都有多个子节点,这都是可能的。

以这种方式使用 FullCalendar 的关键是操作这些资源以及它们如何分组。现在让我们看看如何做到这一点。

代码设置

通常,我期望从某种数据库驱动日记系统。为了使本文及其演示代码与数据库无关,我决定组装一个简单的测试平台。它依赖于创建一系列类,用示例数据创建/填充它们,并使用这些数据来模拟数据库环境。

测试夹具

该平台由多个类组成,这些类代表我想在文章中演示的不同事物。这些是用户、设备、办公室的列表,用户在哪些办公室工作,以及日程/日记事件。该平台定义了类,然后有一个初始化部分,用测试数据填充平台。当用户(就是您!)运行演示代码时,应用程序会检查用户/临时文件夹中是否存在平台的序列化(XML)表示形式,如果存在,则加载它,如果不存在,则初始化自身并创建测试数据。我只会在本文中介绍设置代码的重点,因为主要重点是如何使用资源功能来增强日记。如果您想更详细地了解设置和其他代码,请下载代码!

正在设置 TestHarness...

    public class TestHarness
    {
        public List<BranchOfficeVM> Branches { get; set; }
        public List<ClientVM> Clients { get; set; }
        public List<EquipmentVM> Equipment { get; set; }
        public List<EmployeeVM> Employees { get; set; }
        public List<ScheduleEventVM> ScheduleEvents { get; set; }
        public List<ScheduleEventVM> UnassignedEvents { get; set; }

        // constructor
        public TestHarness()
        {
            Branches = new List<BranchOfficeVM>();
            Equipment = new List<EquipmentVM>();
            Employees = new List<EmployeeVM>();
            ScheduleEvents = new List<ScheduleEventVM>();
            UnassignedEvents = new List<ScheduleEventVM>();
            Clients = new List<ClientVM>();
        }
      ... <etc>

正在初始化列表以接收数据...

        // initial setup if none already exists to load
        public void Setup()
        {
            initClients();
            initUnAssignedTasks();
            initBranches();
            initEmployees();
            linkEmployeesToBranches();
            initEquipment();
            initEvents();
        }
正在填充部分属性数据...

在此,我们创建了一个分支办公室列表供使用。

        public void initBranches()
        {
            var b1 = new BranchOfficeVM();
            b1.BranchOfficeID = Guid.NewGuid().ToString();
            b1.Name = "New York";
            Branches.Add(b1);
            var b2 = new BranchOfficeVM();
            b2.BranchOfficeID = Guid.NewGuid().ToString();
            b2.Name = "London";
            Branches.Add(b2);
        }

我们创建了一些测试 employeeclient...

        public void initEmployees()
        {
            var v1 = new EmployeeVM();
            v1.EmployeeID = Guid.NewGuid().ToString();
            v1.FirstName = "Paul";
            v1.LastName = "Smith";
            Employees.Add(v1);

            var v2 = new EmployeeVM();
            v2.EmployeeID = Guid.NewGuid().ToString();
            v2.FirstName = "Max";
            v2.LastName = "Brophy";

            Employees.Add(v2);
            var v3 = new EmployeeVM();
            v3.EmployeeID = Guid.NewGuid().ToString();
            v3.FirstName = "Rajeet";
            v3.LastName = "Kumar";
            Employees.Add(v3);
     ... <etc>
        public void initClients()
        {
            Clients.Add(new ClientVM("Big Company A", "New York"));
            Clients.Add(new ClientVM("Small Company X", "London"));
            Clients.Add(new ClientVM("Big Company B", "London"));
            Clients.Add(new ClientVM("Big Company C", "Mumbai"));
            Clients.Add(new ClientVM("Small Company Y", "Berlin"));
            Clients.Add(new ClientVM("Small Company Z", "Dublin"));
        }

在各种测试数据之间创建关系...

        public void linkEmployeesToBranches()
        {
            var EmployeeUtil = new EmployeeVM();

            Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Paul"));
            Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Max"));
            Branches[0].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Rajeet"));
            Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Philippe"));
            Branches[1].Employees.Add(EmployeeUtil.EmployeeByName(Employees, "Samara"));
       ... <etc>

在完成支持数据后,我们然后放入一些日记事件本身的示例数据。

(顺便说一句,再次重申,本文重点介绍日记实现的资源和重复功能。关于重要字段、使用功能等的完整解释都在我之前的Full Calendar 入门文章中进行了讨论。如果您是 FullCalendar 日记创建新手,您应该先阅读那篇文章,然后再阅读这篇文章!)

        public void initEvents()
        {
            var utilBranch = new BranchOfficeVM();
            var EmployeeUtil = new EmployeeVM();

            var s1 = new ScheduleEventVM();
            s1.BranchOfficeID = utilBranch.GetBranchByName(Branches, "New York").BranchOfficeID;
            var c1 = utils.GetClientByName(Clients, "Big Company A");
            s1.clientId = c1.ClientID;
            s1.clientName = c1.Name;
            s1.clientAddress = c1.Address;
            s1.title = "Event 2 - Big Company A";
            s1.statusString = Constants.statusBooked;

            var v1 = EmployeeUtil.EmployeeByName(Employees, "Paul");
            s1.EmployeeId = v1.EmployeeID;
            s1.EmployeeName = v1.FullName;
            s1.DateTimeScheduled = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 
                                   DateTime.Now.Day, 11, 15, 0);
            s1.durationMinutes = 120;
            s1.duration = s1.durationMinutes.ToString();
            s1.DateTimeScheduledEnd = s1.DateTimeScheduled.AddMinutes(s1.durationMinutes);
            ScheduleEvents.Add(s1);

      ... <etc>

我们还创建了一些示例“未安排/未分配的日记事件”。这将用于演示资源/调度器附加组件的一些特定功能,这些功能与主日记控件不同。标准事件与尚未分配给日记事件的事件之间的主要区别。

        public void initUnAssignedTasks()
        {
            var uaItem1 = new ScheduleEventVM();
            var cli1 = utils.GetClientByName(Clients, "Big Company A");
            uaItem1.clientId = cli1.ClientID;
            uaItem1.clientName = cli1.Name;
            uaItem1.clientAddress = cli1.Address;
            uaItem1.title = cli1.Name + " - " + cli1.Address;
            uaItem1.durationMinutes = 30;
            uaItem1.duration = uaItem1.durationMinutes.ToString();
            uaItem1.DateTimeScheduled = DateTime.Now.AddDays(14);
            uaItem1.DateTimeScheduledEnd = uaItem1.DateTimeScheduled.AddMinutes
                                                   (uaItem1.durationMinutes);
            uaItem1.notes = "Test notes 1";
      ... <etc>

Full Calendar JavaScript 配置

要设置插件及其资源,我们需要在浏览器加载时初始化它。我的另一篇文章中讨论了通用选项和属性的完整描述。因此,我将在此处展示 JavaScript 设置代码,并讨论它与资源的关系。如果您需要有关整体日记设置的详细信息,请参考另一篇文章。

首先,是通用设置的开始...

// Main code to initialise/setup and show the calendar itself.
  function ShowCalendar() {
     $('#calendar').fullCalendar({
       schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source', // change depending on license type
       theme: false,
       resourceAreaWidth: 230,
       groupByDateAndResource: false,
       editable: true,
       aspectRatio: 1.8,
       scrollTime: '08:00',
       timezone: 'local',
       droppable: true,
       drop: function
        ...<snip> ...

然后是关于资源的重要部分....

// this is where the resource loading for laying out the page is triggered from

resourceLabelText: "@Model.ResourceTitle",  // set server-side
resources:
{
url: '/Home/GetResources',
data: {resourceView : "@Model.DefaultView"},
type: 'POST',

  ...<snip> ...

在这种情况下,我告诉它从服务器端 ajax 控制器 '/Home/GetResources' 获取资源的源。

设置资源有多种选项,您可以使用内联数组ajax/json 源或函数

这是一个数组示例

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        },
        {
           id: 'b',
            title: 'Room B'
        }
    ]
});

日记事件对象与资源之间的关系

Full Calendar 显示事件对象。这是一个添加单个基本事件的基本结构:

events: [
        {
            id: '1',
            title: 'Meeting',
            start: '2015-02-14'
        }

假设我们有以下资源结构

resources:
     [ {
          id: 'a',
          title: 'Room A'
     } ]

资源的唯一 ID 是 'a',要将其链接到我们的事件,并使其显示在相应的资源列/行中,我们只需告诉事件它正在使用该资源 ID

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        }
    ],
    events: [
        {
            id: '1',
            resourceId: 'a',
            title: 'Meeting',
            start: '2015-02-14'
        }
    ]
});

我们还可以将一个事件与多个不同的资源关联 - 在这种情况下,我们用逗号分隔 ID,并使用复数 'resourceIds' 来定义关系,而不是用于单个链接的单数。

$('#calendar').fullCalendar({
    resources: [
        {
            id: 'a',
            title: 'Room A'
        },
        {
            id: 'b',
            title: 'Room B'
        }
    ],
    events: [
        {
            id: '1',
            resourceIds: ['a', 'b'],
            title: 'Meeting',
            start: '2015-02-14'
        }
    ]
});

现在,这里有一些需要理解的内容…… resourceId 是一个动态目标。我的意思是,根据您使用的视图类型,其 ID 具有不同的含义。例如,如果当前视图是“timeline: equipment”,则 ResourceID 指的是 EquipmentID。如果当前视图是“timeline: Employees”,则 ResourceID 指的是 EmployeeID。原因在于,时间线视图的日记网格顶部显示日期/时间(列),行则保留给主要“日记事件”本身,即具有焦点的资源(employeeequipment 等)。

在资源视图之间切换/加载数据

看代码是一回事,但图片更能说明问题!……这是一个截图,显示了用户可用于在不同资源视图之间切换的弹出窗口。

在选择器表单的 OnClick/close 模态事件中,JavaScript 代码获取用户想要查看的资源类型的“值”,然后将其发布到服务器,调用 'setView'。我决定使用 post 而不是 get,因为我不想让我的 URL 变丑!SetView 控制器在 session 数据中设置新的默认视图,然后重定向回索引控制器,然后将正确的视图呈现给用户。

            $('#btnUpdateView').click(function () {
                var selectedView = $('input[name="rdoResourceView"]:checked').val();
                post('/Home/setView', { ResourceView: selectedView }, 'setView');
            });

服务器端代码

服务器端日记的起点是 home/index 控制器。请注意,在控制器顶部声明了 TestHarness 类。Index 接受一个参数“ResourceView”。这告诉控制器要返回哪个视图 - 在我们的例子中,是 Branch/Employee 视图,或设备视图。默认情况下,它返回 Branch/Employee 视图。当索引加载时(像所有其他控制器一样),它会加载 TestHarness - 对于生产环境,您将在此处连接到数据库并加载相应的数据。在此示例中,如果 TestHarness XML 数据不存在,它将通过调用 TestHarness.Setup() 方法来创建它。

Index.cshtml 页面包含一个模型“FullCal.ViewModels.Resource”。它可以携带您想要的任何信息。此示例使用它来携带页面重新加载时显示的“默认视图”以及资源列的标题 string。在控制器中,一旦我们确定/设置了要使用的视图,我们就将“ResourceView”作为 string 返回到视图模型。关于“ResourceView”的最后一件事是,它被存储为“Session 值”。有关实现,请参见控制器方法“setView”(如下)。

  1. Index 控制器
       public class HomeController : Controller
        {
            TestHarness testHarness; // used to store temp data repository
            
            public ActionResult Index(string ResourceView)
            {
                // create test harness if it does not exist
                // this harness represents interaction you may replace with database calls.
                if (Session["ResourceView"]!=null) // refers to whatever default view you may have, 
                                                   // for example Offices/Users/Shared Equipment/etc.
                                                   // you can create any amount and combination 
                                                   // of different view types you need.
                    ResourceView = Session["ResourceView"].ToString();
    
                if (!System.IO.File.Exists(utils.GetTestFileLocation()))
                    {
                    testHarness = new TestHarness();
                    testHarness.Setup();
                    utils.Save(utils.GetTestFileLocation(), testHarness);
                    }
    
                if (ResourceView == null)
                    ResourceView = "";
    
                ResourceView = ResourceView.ToLower().Trim();
    
                var DiaryResourceView = new Resource();
    
                if (ResourceView == "" || ResourceView == "employees") // set the default
                    { 
                    DiaryResourceView.DefaultView = "employees";
                    DiaryResourceView.ResourceTitle = "Branch offices";
                    }
                else if (ResourceView == "equipment")
                    {
                    DiaryResourceView.DefaultView = ResourceView;
                    DiaryResourceView.ResourceTitle = "Equipment list";
                    }
    
                return View(DiaryResourceView);
            }
  2. SetView 控制器
            // this method, called from the index page, sets a session variable for 
            // the user that gets looped back to the index page to tell it what view 
            // to display. branch/employee or Equipment.
    
            public ActionResult setView(string ResourceView)
            {
              Session["ResourceView"] = ResourceView;
              return  RedirectToAction("Index");
            }

有用功能

管理资源单元格导航 - 一个小麻烦....

当我们拥有单个资源视图且没有子资源时,当我们单击一个单元格以创建新事件时,很明显我们单击的是代表单个资源的单元格....

然而,当我们拥有父子关系时,情况就不同了,我们必须跟踪我们的位置,并根据用户单击的位置实现规则...

为了帮助管理这种情况,我们维护一个内存中的资源及其关联列表,然后使用 FullCalendarOnClick/Select 事件来查找所选单元格并决定是否需要实现规则。

// use this function to get a local list of employees/branches/equipment etc 
// and populate arrays as appropriate for checking business rules etc.

function GetLocationsAndEmployees() {
    $.ajax({
           url: '/home/GetSetupInfo',
           cache: false,
           success: function (resultData) {
                    ClearLists();
                    EmployeeList = resultData.Employees.slice(0);
                    BranchList = resultData.Branches.slice(0);
                    EquipmentList = resultData.Equipment.slice(0);
                    ClientList = resultData.Clients.slice(0); 
                }
         });

在此示例中,我们不允许用户单击“office”行,因此如果用户出错,我们会发出警报...

var employeeResource =
       EmployeeList.find( // if the row clicked on is NOT in the known array of employeeID,
                          // then drop out (and alert...)
                         function (employee) 
                                { return employee.EmployeeID == resourceObj.id; }
                        )

拖放

拥有将事件拖放到日记上的功能很有用。但是,我们需要在 drop 事件上告诉日记有关被拖放的事件的信息。为此,我们将信息以特定方式附加到要拖放的对象上。

为了识别要拖放的项目,我们使用类 'draggable' 来标记它们。要将数据附加到它们,我们调用一个函数,该函数遍历所有标记为该类的内容,并分配数据如下

// set up for drag/drop of unassigned tasks into scheduler 
// *example only - if using a large data feed from a table*

        function InitDragDrop() {
            $('.draggable').each(function () {
                // create an Event Object 
                // ref: (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
                // it doesn't need to have a start or end
                var table = $('#UnScheduledEvents').DataTable();

                var eventObject = {
                    id: $(table.row(this).data()[0]).selector,
                    clientId: $(table.row(this).data()[1]).selector,
                    start: $(table.row(this).data()[2]).selector,
                    end: $(table.row(this).data()[3]).selector,
                    title: $(table.row(this).data()[4]).selector,
                    duration: $(table.row(this).data()[5]).selector,
                    notes: $(table.row(this).data()[6]).selector,
                    color: 'tomato'
                }

                // gotcha: MUST be named "event", for *external dropped objects* and 
                // some rules:   http://fullcalendar.io/docs/dropping/eventReceive/
                $(this).data('event', eventObject);

                // make the event draggable using jQuery UI
                $(this).draggable({
                    activeClass: "ui-state-hover",
                    hoverClass: "ui-state-active",
                    zIndex: 999,
                    revert: true,      // will cause the event to go back to its
                    revertDuration: 0  //  original position after the drag
                });
            });
        };

当项目被拖放时,它会被 FullCalendar 的 'eventReceive' 方法挂钩。在我们的示例中,我们要求用户在继续之前进行确认

eventReceive: function (event) { 
var confirmDlg = confirm('Are you sure you wish to assign this event?');

if (confirmDlg == true) {
    var eventDrag = {
        title: event.title,
        start: new Date(event.start),
        resourceId: event.resourceId,
        clientId: null,
        duration: 30,
        equipmentId: null,
        BranchID: null,
        statusString: "",
        notes: "",
    }
    UpdateEventMove(eventDrag, null);
}

现在,这里有一个要注意的小麻烦....您可能还记得前面在文章中讨论过的事件对象的 'resourceId' 属性是“动态目标”……好吧,这里是它的实际应用。根据用户正在查看的视图(例如员工或设备),关联事件中的 'resourceId' 值将*要么*引用员工 ID*要么*引用设备 ID。因此,我们在此处检查视图类型,然后再发送数据到服务器。当然,这是我的示例实现,有许多方法可以处理逻辑 - 关键在于指出您需要考虑它。

function UpdateEventMove(event, view) {
    // determine the view and from this set the correct EmployeeID or ResourceID 
    // before sending down to server
    if (ResourceView == 'employees')
        event.employeeId = event.resourceId;
    else {
        event.employeeId = $('#newcboEmployees').val();
    }
    var dataRow = {
        'Event': event
    }

    $.ajax({
        type: 'POST',
        url: "/Home/PushEvent",
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(dataRow)
    });
}

重复/周期性日历事件

我想演示的最后一件事是我们如何将重复或周期性事件放入我们的解决方案中。我使用了两个非常有用的开源库的组合来实现这一点,一个在浏览器端运行,一个在服务器端运行。在我们查看我们使用的代码和组件之前,了解重复事件的工作原理以及我们如何表示周期性事件在我们的解决方案中会很有帮助。

理解 CRON 格式

CRON JOB(来自 chronological,即按时间顺序)在 UNIX 操作系统中是一个众所周知的命令,用于告诉系统在特定时间执行作业。CRON 具有一种语法,当制定好后,计算机和人类都可以阅读,它描述了作业应触发的确切日期/时间,并且非常灵活。CRON string 不仅限于描述单个日期和时间(例如:2016 年 1 月 1 日),它还可以用于描述重复的时间模式(例如:每月第三天上午 9 点)。CRON 语法有几种不同的实现方式,如果需要,您可以自行扩展。例如,如果基本 CRON 描述符只允许重复事件,您可能会决定在您的业务逻辑中添加“在 X 日期和 Y 日期之间”的限制(例如:“每月第三天的上午 9 点,但仅当那天是星期二且介于 2016 年 6 月 1 日和 2016 年 8 月 30 日之间”)。

标准 CRON 由五个字段组成,每个字段用空格分隔

minute hour day-of-month month-of-year day-of-week

每个字段可以包含一个或多个描述字段内容的字符

  • *(星号)表示所有值。例如,如果我们在分钟字段中有“*”,则表示“每分钟执行一次作业”。如果它在月份字段中,则表示“每月执行一次作业”。
  • 字段中的逗号用于分隔值(例如:2,4,6... 在第 2、4 和第 6 分钟)
  • 字段中的连字符用于指定范围(例如:4-9... 在第 4 和第 9 分钟之间)
  • 在星号或连字符范围之后,我们可以使用斜杠“/”来表示值以固定的间隔重复。(例如:0-18/2 表示在 0 点到 18 点之间每两小时执行一次作业)

以下是一些展示 CRON 实际应用的示例

* * * * * *                         Each minute
45 17 7 6 * *                       Every  year, on June 7th at 17:45
* 0-11 * * *                        Each minute before midday
0 0 * * * *                         Daily at midnight
0 0 * * 3 *                         Each Wednesday at midnight

您可以在这里(部分示例来源)和这里找到有关 CRON 的更详细信息。

JQueryUI Cron Builder

在可能的情况下,我尽量不重复造轮子。几年前,我遇到了 Shawn Chin 非常有用的JQuery-Cron builder,并且之后在多个项目中成功使用过。这个 JQuery 插件不像强迫用户输入晦涩的表达式来指定 cron 表达式那样,而是允许用户从易于使用的 GUI 中选择重复时间。它设计为一系列下拉框,用户从中选择值。根据初始选择,界面会发生变化,提供适当的后续值。一旦用户设置了所需的重复/周期性时间,您就可以调用一个函数,该函数会返回用户所选可视化值对应的 CRON 表达式。

使用 Cron builder 通常是 JQuery 风格。我们声明一个 div,然后对它调用插件

<div id="repeatCRON"></div>
 $('#repeatCRON').cron();

以下是它在浏览器中呈现的一些示例

当我们保存日记事件时,我们查询 CronBuilder 插件并获取 CRON 表达式 - 一旦我们有了它,我们就可以将其作为 string 保存到数据库的字段中... 在我的例子中,我将该字段命名为“Repeat”。

$('#submitButton').on('click', function (e) {
    e.preventDefault();
    SelectedEvent.title = $('#title').val();
    SelectedEvent.duration = $('#duration').val();
    SelectedEvent.equipmentId = $('#cboEquipment').val();
    SelectedEvent.branchId = $('#branch').val();
    SelectedEvent.clientId = $('#cboClient').val();
    SelectedEvent.notes = $('#notes').val();
    SelectedEvent.resourceId = $('#cboEmployees').val();
    SelectedEvent.statusString = $('#cboStatus').val();
    SelectedEvent.repeat = $('#repeatCRONEdit').cron("value")
    UpdateEventMove(SelectedEvent, null);
    doSubmit();
});

我们也可以做相反的事情 - 获取一个 CRON string,然后将其传递给 JQuery builder,它将显示表示 CRON 表达式的用户界面。

if (SelectedEvent.repeat != null)
    $('#repeatCRONEdit').cron("value", SelectedEvent.repeat);
else
    $('#repeatCRONEdit').cron("value", "* * * * *");

NCronTab

好的,现在我们已经完成了重复/周期性事件信息的用户界面构建和存储 - 现在让我们来看看我们能做什么来决定*何时/是否*在我们的日记中显示这些重复事件。

在 UNIX 中,CronTab 是一个文件,其中包含 CRON 表达式列表,以及一个系统一旦触发该 CRON 时间就需要执行的命令。从 Windows 的角度来看,等效的是Windows 任务计划程序服务

NCrontab 是一个用 C# 6.0 编写的库,它提供了以下功能

  • 解析 crontab 表达式
  • 格式化 crontab 表达式
  • 根据 crontab 计划计算时间出现次数

该库不提供任何计划程序,也不是像 Unix 平台上的 cron 那样的计划功能。它提供的是解析、格式化和一种算法,用于根据 crontab 格式表示的给定计划生成时间出现次数。(来源:NCrontab

在此示例项目中,我正在使用 NCrontab 库方法来检查存储的 CRON 表达式字符串,与用户在 FullCalendar 中选择的日期范围进行比较,并确定我的存储的重复值是否在此日期范围内出现。

让我们看看代码是如何工作的

  1. 用户决定刷新日记以显示特定日期范围内的事件。请注意,发送的参数是 startend 日期以及所需的 resourceView
    public JsonResult GetScheduleEvents(string start, string end, string resourceView)
    .. <etc>
  2. 我们查询所有计划事件,查找任何具有“repeat”值存储的事件。
    repeatEvents = testHarness.ScheduleEvents.Where(s => (s.repeat != null));
  3. 然后,我们检查每个重复事件(即 CRON string 表达式),并使用 NCronTab.CrontabSchedule 对其进行*解析*,将解析结果传递给一个方法,该方法指示“在给定的开始和结束日期之间,给我一个 CRON string 所代表的有效日期/时间的列表。”
    if (repeatEvents!=null)
    foreach (var rptEvnt in repeatEvents)
    {
        var schedule = CrontabSchedule.Parse(rptEvnt.repeat);
        var nextSchdule = schedule.GetNextOccurrences(Start, End);
        foreach (var startDate in nextSchdule)
        { 
            ScheduleEvent itm = new ScheduleEvent();
            itm.id = rptEvnt.EventID;
            if (rptEvnt.title.Trim() == "")
                itm.title = rptEvnt.clientName;
            else itm.title = rptEvnt.title;
            itm.start = startDate.ToString("s");
            itm.end = startDate.AddMinutes(30).ToString("s");
            itm.duration = rptEvnt.duration.ToString();
            itm.notes = rptEvnt.notes;
            itm.statusId = rptEvnt.statusId;
            itm.statusString = rptEvnt.statusString;
            itm.allDay = false;
            itm.EmployeeId = rptEvnt.EmployeeId;
            itm.clientId = rptEvnt.clientId;
            itm.clientName = rptEvnt.clientName;
            itm.equipmentId = rptEvnt.equipmentID;
            itm.EmployeeName = rptEvnt.EmployeeName;
            itm.repeat = rptEvnt.repeat;
            itm.color = rptEvnt.statusString;
            if (resourceView == "employees")
                itm.resourceId = rptEvnt.EmployeeId;
            else itm.resourceId = rptEvnt.equipmentID;
            EventItems.Add(itm);
        }
    }

最后要提到的是上面的代码的最后几行 - 再次,我们回到“动态目标”问题。根据用户选择的视图,我们需要设置正确的 resourceId 值,以便在它返回到浏览器后能够正确渲染。

结论

这基本就结束了本文。如果您需要实现一个功能齐全的日记/日历/约会解决方案,能够以非常强大的方式整合多资源,您应该大力考虑 FullCalendarJQueryCronNCronTab 的组合。我附加了一个工作示例,其中包含本文中讨论的所有功能 - 请下载并尝试使用它。请注意,这不是一个独立的工程 - 它演示了特定的功能。您应该将其与我另一篇解释 FullCalendar 的文章(及其附带代码)结合使用,以实现您自己的特定工作解决方案。

最后,一如既往,如果您喜欢本文,请考虑投票支持!

历史

  • 2016 年 9 月 3 日:版本 1
© . All rights reserved.