Creating Custom Widget

Topics: Customizing Orchard, Writing modules
Aug 1, 2012 at 5:10 PM
Edited Aug 1, 2012 at 5:10 PM

I am having trouble with my first custom orchard module.  I want to be able to store a message that is displayed as a content part.  Its called SiteAlert and I am linking to it from the admin menu.

I am having difficulty with saving my SiteAlert message and then displaying it again.  Sticking a break point on Admin controller showed it's accepting my value but I'm struggling to get it saved and therefore displayed on edit screen.   I believe it may be something on the Http post section of controller but I may be wrong. 

Any advice would be appreciated as its starting to frustrate me and I've included any code I think may be evn be vaguely relevant.    Thanks

AdminController.cs

namespace Orchard.SiteAlert.Controllers
{
    [ValidateInput(true), Admin]
    public class AdminController : Controller
    {
        private IShapeFactory _shapeFactory;
        private readonly IContentManager _contentManager;
        private readonly ISiteAlertService _alertService;

        public AdminController(ISiteAlertService alertService, IContentManager contentManager, IShapeFactory shapeFactory)
        {

            _shapeFactory = shapeFactory;
            _contentManager = contentManager;
            _alertService = alertService;
        }


        [HttpGet]
        public ActionResult Index()
        {
            // Create the viewmodel
            SiteAlertWidgetViewModel model = new SiteAlertWidgetViewModel { SiteAlert = _alertService.GetSiteAlert() };

            return View(model);  // break point shos taking in value entered

        }

        [HttpPost]
        public ActionResult Index(SiteAlertWidgetViewModel model)
        {
            // ?
            return View(model);
        }
    }
}

 

SiteAlertWidgetDriver.cs

namespace Orchard.SiteAlert.Drivers
{
    public class SiteAlertWidgetDriver : ContentPartDriver<Models.SiteAlertWidgetPart>
    {
       
        private readonly ISignals signals;
        private readonly ISiteAlertService alertService;

        
        public SiteAlertWidgetDriver(ISiteAlertService service, ISignals sig)
        {
            signals = sig;   // used to display message when site Alert Changed
            alertService = service;
        }

        //GET
        protected override DriverResult Display(SiteAlertWidgetPart part, string displayType, dynamic shapeHelper)
        {
                             
            return ContentShape("Parts_SiteAlertWidget", 
                () => shapeHelper.Parts_SiteAlertWidget(
                    SiteAlert: alertService.GetSiteAlert()
                ));
        }

             // GET
        protected override DriverResult Editor(SiteAlertWidgetPart part, dynamic shapeHelper)
        {

            var viewModel = new SiteAlertWidgetViewModel
                                {
                                    SiteAlert = part.SiteAlert
                                };

            return ContentShape("Parts_SiteAlertWidget_Edit",
                                () => shapeHelper.EditorTemplate(
                                    TemplateName: "Parts/SiteAlertWidget",
                                    Models: viewModel,
                                    //Model: new SiteAlertWidgetViewModel
                                    //{
                                   //    SiteAlert = part.SiteAlert
                                   //},                                                
                                   Prefix: Prefix)).OnGroup("Site Alert");  // linked to the handler
        }

        // POST
        // displays the update message for when alert content changed
        protected override DriverResult Editor(SiteAlertWidgetPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            signals.Trigger("Orchard.SiteAlert.Changed");
            return Editor(part, shapeHelper);
        }
    }
}

 

SiteAlertWidgetRecordHandler.cs

namespace Orchard.SiteAlert.Handlers
{

    public class SiteAlertWidgetRecordHandler : ContentHandler
    {
        public SiteAlertWidgetRecordHandler(IRepository<SiteAlertWidgetRecord> repository)
        {
            T = NullLocalizer.Instance;
            Filters.Add(StorageFilter.For(repository));
            Filters.Add(new ActivatingFilter<SiteAlertWidgetPart>("Site"));
        }

        public Localizer T { get; set; }

        protected override void GetItemMetadata(GetContentItemMetadataContext context)
        {
            if (context.ContentItem.ContentType != "Site")
                return;
            base.GetItemMetadata(context);
            context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Site Alert"))); 
        }
    }
}

 

ISiteAlertService.cs

namespace Orchard.SiteAlert.Services
{
    public interface ISiteAlertService : IDependency
    {
        string GetSiteAlert();
    }
}

 

SiteAlertService.cs

 

namespace Orchard.SiteAlert.Services
{
    public class SiteAlertService : ISiteAlertService
    {
        private readonly ICacheManager _cacheManager;
        private readonly ISignals _signals;
        private readonly IWorkContextAccessor _wca;

        public SiteAlertService(IWorkContextAccessor wca, ICacheManager cacheManager, ISignals signals)
        {
            _wca = wca;
            _cacheManager = cacheManager;
            _signals = signals;
        }

        //gets sitealert message from the database


        public string GetSiteAlert()
        {
            try
            {

                return _cacheManager.Get(
                    "Orchard.SiteAlert",
                    ctx =>
                        {

                            ctx.Monitor(_signals.When("Orchard.SiteAlert.Changed"));
                            var workContext = _wca.GetContext();
                            var siteSettings =
                                (Models.SiteAlertWidgetPart) workContext
                                                                 .CurrentSite
                                                                 .ContentItem
                                                                 .Get(typeof (Models.SiteAlertWidgetPart));
                            return siteSettings.SiteAlert;

                        });
            }
            catch
            {
                return String.Empty;

            }

        }
    }
}

 

Views/Parts/ SiteAlertWidgetViewModel.cshtml

 

@{
if (!string.IsNullOrEmpty(Model.SiteAlert.ToString()))
{
    <div id="alert">
        <h3>
            Alert</h3>
        <p>
            @Html.Raw(Model.SiteAlert)</p>
    </div>
}
}

 

Views/Editor Templates/Parts/ SiteAlertWidgetViewModel.cshtml

 

@model Orchard.SiteAlert.Models.SiteAlertWidgetPart
@*@model Orchard.SiteAlert.ViewModels.SiteAlertWidgetViewModel*@

<fieldset>
    <legend>Site Alert - Edit</legend>
    <!-- Site wide Alert -->
    <div>
        <label for="SiteAlert">@T("Site wide alert")</label>
        @Html.TextBoxFor(m=>m.SiteAlert, new { @class = "text-box single-line", style="width:500px" })
    </div>

    <hr />

</fieldset>
Developer
Aug 2, 2012 at 11:54 AM

I'm a bit confused: in the driver, you are using the SiteAlertWidgetViewModel as the model for your shape: 

     // GET
        protected override DriverResult Editor(SiteAlertWidgetPart part, dynamic shapeHelper)
        {

            var viewModel = new SiteAlertWidgetViewModel
                                {
                                    SiteAlert = part.SiteAlert
                                };

            return ContentShape("Parts_SiteAlertWidget_Edit",
                                () => shapeHelper.EditorTemplate(
                                    TemplateName: "Parts/SiteAlertWidget",
                                    Models: viewModel,
                                    //Model: new SiteAlertWidgetViewModel
                                    //{
                                   //    SiteAlert = part.SiteAlert
                                   //},                                                
                                   Prefix: Prefix)).OnGroup("Site Alert");  // linked to the handler
        }

Yet, in the POST version of the Editor, you are updating the SiteAlertWidgetPart:

// POST
        // displays the update message for when alert content changed
        protected override DriverResult Editor(SiteAlertWidgetPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            signals.Trigger("Orchard.SiteAlert.Changed");
            return Editor(part, shapeHelper);
        }

Perhaps that works as long as the two classes both have the SiteAlert property, but maybe put a breakpoint in the POST version of the Editor to verify that the values are posted back as expected (after the call to TryUpdateModel of course).

 

Aug 2, 2012 at 1:17 PM
Edited Aug 2, 2012 at 1:18 PM

Now that you've pointed it out it I can see what you mean.  It does seem to work as it worked when the code was used as a normal widget(child of settings in menu), However I am not able to confirm it still works using the break point to confirm that at present as I haven't worked out how to save the value yet.  I'm optomistioc my code for posting should work once I've manage to save it but only time will tell with that. 

Developer
Aug 2, 2012 at 1:25 PM
Edited Aug 2, 2012 at 1:26 PM

Oh, so you are looking to use your own controller to invoke the Updates of the parts, yes? That explains the question mark in your controller. To see how that works, simply have a look at the BlogPostAdminController. ContentManager service is the key.

Aug 2, 2012 at 2:15 PM

Yeah that's it exactly.  I forgot all about that question mark that i put there when posting the code.  I'm looking at your suggestion now and it looks like its what Im looking for, certainly more so than the orchard.alias stuff I was looking at before 

Aug 3, 2012 at 11:54 AM

I'm still having some issues with this module.  I've been looking at BlogPost and found a few similar issues on the forum like (http://orchard.codeplex.com/discussions/347341) but still not saving and re displaying the siteAlert message.  The widget does work if i treat it like a normal widget (through widget section on admin menu).  Can anyone offer advice?

AdminController.cs (current version)

namespace Orchard.SiteAlert.Controllers
{
    [ValidateInput(true), Admin]
    public class AdminController : Controller, IUpdateModel
    {
        private IShapeFactory _shapeFactory;
        private readonly IContentManager _contentManager;
        private readonly ISiteAlertService _alertService;
        private readonly IOrchardServices _orchardServices;

        public AdminController(ISiteAlertService alertService, IContentManager contentManager,
                               IShapeFactory shapeFactory, IOrchardServices orchardServices)
        {
            _shapeFactory = shapeFactory;
            _contentManager = contentManager;
            _alertService = alertService;
            _orchardServices = orchardServices;
        }


        [HttpGet]
        public ActionResult Index()
        {
            // Create the viewmodel
            SiteAlertWidgetViewModel model = new SiteAlertWidgetViewModel { SiteAlert = _alertService.GetSiteAlert() };

            return View(model); // break point shos taking in value entered

        }

        //[HttpPost]
        //public ActionResult Index(SiteAlertWidgetViewModel model)
        //{
        //    // ?
        //    return View(model);
        //}


        // Taken from BlogAdminController.cs - Blog Module
        [HttpPost]
        public ActionResult Index(string SiteAlert)
        {
            // Create as ContentItem:
            //dynamic siteAlert = _alertService.GetSiteAlert();
            var siteAlert = _contentManager.New("SiteAlertWidget");


            var model = _orchardServices.ContentManager.UpdateEditor(siteAlert, this);
            if (!ModelState.IsValid)
            {
                // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
                return View((object) model);
            }
            // Now save the full ContentType:
            _contentManager.Create(siteAlert);

            return RedirectToAction("Create");
        }

        bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties)
        {
            return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
        }

        void IUpdateModel.AddModelError(string key, LocalizedString errorMessage)
        {
            ModelState.AddModelError(key, errorMessage.ToString());
        }
    }
}

Coordinator
Aug 3, 2012 at 6:46 PM

Why are you creating a new site alert record every time? Your service is retrieving the site alert from the site settings content item, but when you're saving, you're creating a new record. There is not a chance this would go where it's supposed to go, which is on the site settings item. Instead, you need to retrieve the site settings item and update that.

Aug 14, 2012 at 5:19 PM

Thanks for responses so far

After a week on doing other work, I've returned to this and I have made some progress but still unable to get it working properly. 

At present the code is retrieving the site alert message from the service successfully (tested by hard coding in value) and it's even saving this value, but I don't know how to hard code it to save user entry, either via create or edit views.

What I have is below, but what's also happening is when I click submit on index (index and edit will both point to index view) or create view it's gives 404 error on url "http://localhost:50685/Orchard.SiteAlert/SiteAlertWidget".  I know this specific issue is from line in edit and index view at top of form but not sure why its not liking the entries now.  I suspect I['m missing something really obvious but the line is 

@using(Html.BeginForm("Create", "SiteAlertWidgetPart"))  

I know I have a few issues and that the code is 'untidy' but any help with saving the user entry would again be really appreciated.  

 

 

admin controller.cs
namespace Orchard.SiteAlert.Controllers
{
    [ValidateInput(true), Admin]
    public class AdminController : Controller, IUpdateModel
    {
        private IShapeFactory _shapeFactory;
        private readonly IContentManager _contentManager;
        private readonly ISiteAlertService _alertService;
        private readonly IOrchardServices _orchardServices;

        public AdminController(ISiteAlertService alertService, IContentManager contentManager,
                               IShapeFactory shapeFactory, IOrchardServices orchardServices)
        {
            //model = new SiteAlertWidgetViewModel { SiteAlert = _alertService.GetSiteAlert() };
            _shapeFactory = shapeFactory;
            _contentManager = contentManager;
            _alertService = alertService;
            _orchardServices = orchardServices;
        }



        [HttpGet]
        public ActionResult Index()
        {
            // Create the viewmodel
            var model = new SiteAlertWidgetViewModel {SiteAlert = _alertService.GetSiteAlert()};
            return View("Create", model);  // Temp view is create until sort issue with saving user input

        }

        [HttpGet]
        public ActionResult Create()
        {
            //var model = new SiteAlertWidgetViewModel {SiteAlert = _alertService.GetSiteAlert()};
            dynamic model = _contentManager.New<SiteAlertWidgetPart>("SiteAlert");

            // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
            return View("Index", model);
        }

        [HttpPost, ActionName("Create")]
        public ActionResult CreatePOST()
        {
            // Create as ContentItem:
            dynamic siteAlert = _contentManager.New<SiteAlertWidgetPart>("SiteAlert");

            dynamic model = _contentManager.UpdateEditor(siteAlert, this);
            if (!ModelState.IsValid)
            {
                // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
                return View("Index");
            }
            // Now save the full ContentType:
            _contentManager.Create(siteAlert);
            return RedirectToAction("Create", model);
        }


        //look at once sorted create
        //[HttpPost, ActionName("Edit")]
        //public ActionResult EditPOST(string siteAlert)
        //{
        //    dynamic alert = _alertService.GetSiteAlert(siteAlert);
        //    var model = _contentManager.UpdateEditor(alert, this);

        //    if (!ModelState.IsValid)
        //        return View(model);

        //    _notifier.Add(NotifyType.Information, T("Your customer has been saved"));
        //    return RedirectToAction("Edit", new { siteAlert });
        //}


        //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // allows updater to work - copied from BlogAdminController.cs (Blog Module)

        bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties)
        {
            return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
        }

        void IUpdateModel.AddModelError(string key, LocalizedString errorMessage)
        {
            ModelState.AddModelError(key, errorMessage.ToString());
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    }
}

SiteAlertService.cs
namespace Orchard.SiteAlert.Services
{
    public class SiteAlertService : ISiteAlertService
    {
        private readonly ICacheManager _cacheManager;
        private readonly ISignals _signals;
        private readonly IWorkContextAccessor _wca;

        public SiteAlertService(IWorkContextAccessor wca, ICacheManager cacheManager, ISignals signals)
        {
            _wca = wca;
            _cacheManager = cacheManager;
            _signals = signals;
        }

        //gets sitealert message from the database
        public string GetSiteAlert()
        {
            try
            {

                return _cacheManager.Get(
                    "Orchard.SiteAlert",
                    ctx =>
                    {

                        ctx.Monitor(_signals.When("Orchard.SiteAlert.Changed"));
                        var workContext = _wca.GetContext();
                        var siteSettings =
                            (Models.SiteAlertWidgetPart)workContext
                                                             .CurrentSite
                                                             .ContentItem
                                                             .Get(typeof(Models.SiteAlertWidgetPart));
                        //siteSettings.SiteAlert = "Test";  //entered to make sure value retrieved succesfully.  even after commenting out siteAlert saved as Test
                        return siteSettings.SiteAlert;
                        

                    });
            }
            catch
            {
                return String.Empty;
            }

        }
    }
}
views/admin/create.cshtml
@model Orchard.SiteAlert.ViewModels.SiteAlertWidgetViewModel

@Html.AntiForgeryToken()

@using (Html.BeginFormAntiForgeryPost("Create", "SiteAlertWigetPart"))
{
    <fieldset>
        <legend><b>Site Alert - (Create)</b></legend>
        <br />
        <!-- Site wide Alert -->
        <label for="SiteAlert">@T("Site wide alert")</label>
        @Html.TextBoxFor(m => m.SiteAlert, new { @class = "text-box single-line", style = "width:500px" })
        <button type="submit" onclick= "SiteAlertWidgetRecord()">@T("Submit")</button> 
    </fieldset>                                                                                 
}

 

Aug 17, 2012 at 2:36 PM
Edited Aug 17, 2012 at 2:37 PM

I have managed to get this working and wanted to update this encase someone else stumbles across this question.   The main thing from code above that I have made major changes to is the Admin Controller and I have put in the new code below.  

I sorted my issues by watching the very helpful Pluralsight videos (should have watched them sooner) but if you don't have access to this, I'd suggest downloading the module from here that the advanced Pluralsight tutorial creates and look at the solution yourself

 

 

namespace Orchard.SiteAlert.Controllers
{
    [ValidateInput(true), Admin]
    public class AdminController : Controller, IUpdateModel
    {
        private readonly ISiteAlertService _alertService;
        private readonly IRepository<SiteAlertWidgetRecord> _alertRepository;
        private readonly IOrchardServices _orchardServices;

        public AdminController(ISiteAlertService alertService, IOrchardServices orchardServices, IRepository<SiteAlertWidgetRecord> alertRepository)
        {

            _alertService = alertService;
            _orchardServices = orchardServices;
            _alertRepository = alertRepository;
        }

        public Localizer T { get; set; }

        [HttpGet]
        public ActionResult Index()
        {
            var viewModel = new SiteAlertWidgetViewModel {SiteAlert = _alertService.GetSiteAlert()};
            return View(viewModel);

        }
              
        [HttpGet]
        public ActionResult Edit()
        {
            var alert = new SiteAlertWidgetViewModel {SiteAlert = _alertService.GetSiteAlert()};
            return View(alert);
        }

        [HttpPost, ActionName("Edit")]
        public ActionResult EditPost(SiteAlertWidgetViewModel viewModel)
        {
            SiteAlertWidgetRecord alert = _alertRepository.Get(1);
            alert.SiteAlert = viewModel.SiteAlert;
            _alertRepository.Update(alert);
            _orchardServices.Notifier.Add(NotifyType.Information, T("Saved Site Alert Changes"));
            return RedirectToAction("Index");
        }

        // below code allows updater to work
        bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties)
        {
            return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
        }

        void IUpdateModel.AddModelError(string key, LocalizedString errorMessage)
        {
            ModelState.AddModelError(key, errorMessage.ToString());
        }
    }
}