Showing posts with label User eXperience. Show all posts
Showing posts with label User eXperience. Show all posts

Sunday, April 7, 2019

Options considered for extending Stream Live Events with live chat/questions capability

With the recent GA of Stream Live Events, Microsoft delivers a low-entry option to easily setup company-internal webcasts. All you need in addition are on the webcast producing side: 1) recording equipment - camera + audio, and 2) an encoder solution to transmit the recording inputs into the Live Events endpoint. The camera + audio options vary from the onboard webcam + microphone of a laptop, USB-based externals, upto high-end studio-quality equipment. For Stream Live Events handling the sophistication on the producing input side does not make a difference, the inputted RTMP(S) signal is from input endpoint processed the same. Whether an organization also needs some additionals on the consumption side depends on the audience side plus the internal network capacity. Watching a Live Event instantiates an individual / unicast stream per event viewer. Under assumption that the company network is sufficient scaled for nowaday cloud solutions usage, it is feasile to simultanuous watch a 'LIVE' Stream event per network location upto a maximum of few hundreds. In case of bigger audience, Microsoft states that it is needed to reduce the network load via a video distribution approach through a SDN/eSDN solution.
In its first released version, Stream Live Events capabilities are focussed on the core streaming + playing aspects. Business demands on a webcast typical go beyond that. The biggest additional demand is for interaction between the webcast audience and the presenter. Thus a chat window in which individual attendees can submit their questions, and that the presenter can follow + respond on. It is on the roadmap of Stream Live Events to deliver this (note: Stream recent delivered 'quizzes/ polls' capability, but this is targetted at inserting in already recorded videos for a.o. training purposes; not for dynamic interaction with live streaming), but not out-of-the-box delivered nowadays. However, with Office 365 as underlying platform you have as event developer a feature-rich toolbox at hand in which you can deliver yourself a webcast experience in which viewers have the live stream and chatbox combined, via no-code or low-code approaches. The hosting platform of such landingspage is naturally SharePoint Online....; as every created Stream Group is effectively an Office 365 Group, it has an own SharePoint Modern Team Site provisioned. The persons authorized to this site is the same audience that are permitted to join the scheduled Live Event. Create in this site a new page to function as the webcast entrance, and select the '2:1 columns' layout. In the left column embed the Stream Live Event instance - one gets this from Stream via 'Share' and then pick the 'Embed' option. With this setup, all left is to extend with chatbox / questions input control. There you have multiple options. One is to utilize the rich capabilities of Yammer, and embed a Yammer channel on the page. This gives you immediate a 'chat-like' experience, in which one can enter questions, semi-live follow the inputs / chats of others, respond on inputs of others. This immediate visibility of all the responses of ones peers may be unwanted, e.g. due potential sensitivity. In that case an alternative setup is to capture the inputs in a SharePoint list in the site, and only grant the webcast facilitators the authorization to view all received inputs. The webcast attendees submit their questions and feedback via an input form embedded on the SharePoint page. For this on sublevel again multiple alternatives are available. You can embed the out-of-the-box NewForm of the list; customize the NewForm via PowerApps; a MS Forms instance that via MS Flow sends the input to SharePoint List; or build a custom control via SPFx or even as a chatbot via Azure Bot Service. Of these options all except for the latter 2 are no/low-code 'mash-up' compositions, that deliver on the needed job. The decision-weighing between them is mostly on the user-experience and branding, see screen impressions of the various options.

Thursday, December 14, 2017

Automatic Azure AD B2B redemption is not feasible

Azure AD Business-2-Business is the enterprise-ready and secure new approach to enable SharePoint Online external sharing. In this approach, externals are added as guest users in the Azure AD of the inviting company. The guest account in the host Azure AD functions as placeholder to the actual account of the invited guest in external identity administration. Azure AD B2B is an implementation of federated Identity Management.
The process to authorize an external person for access in shared SharePoint Online site consists of 2 steps:
  1. First, Azure AD admin (or anyone who has the “Guest Inviter” role) has to add a guest account to the host Azure AD
  2. Next, site owner can invite the guest account to the external shared site
However, it turns out that there is some usage unclarity and sequence dependency in this process:
  • Usage unclarity: For the first step, the guest receives invite in mailbox to accept / redempt the invite. But next the guest user is redirected to empty Apps page in the tenant of inviting organization ==> no authorizations are granted yet;
  • Sequence dependency: The site owner cannot execute the 2nd step until the Azure AD invite is redempted ==> 'Sharing failed: Sharing to external users is not supported' (which is a misleading / incorrect error message; sharing is supported, yet not to the particular guest account as long (s)he has not redempt the invitation to the hosting Azure AD)
To address the first drawback, which may result in negative first impression with the invited guest ("I only see an empty Apps page, cannot do anything"), you can utilize the PowerShell Azure AD cmdlet 'New-AzureADMSInvitation' with '-SendInvitationMessage' parameter set to false. The result is that the invited guest is not informed yet of the invite to Azure AD, that from the perspective of the guest user is useless anyhow: one can only do something after authorized to a SharePoint site. But the indirect result is that the Azure AD invite is not redeemed - the guest is not made aware nor asked to perform the redemption -, and this results that the site owner cannot add the guest user. A catch22 situation.
A potential way out would be to automatic redempt the Azure AD invitation on behalf of the invited guest. However, this is only possible in case the organization of invited guest itself has an Azure AD tenant. Although the usage of Azure is growing in the market, there is and will remain a large set of organizations that have not themselves an Azure subscription + tenant. And then the automatic redemption on behalf of guest users is not possible.

Saturday, July 15, 2017

Approach to performant display from a SPList with multiple Lookups + Person fields

Issue: business users complain that the page loading of a SharePoint listview takes a long time, up to a minute. During page loading + rendering, the browser is totally unresponsive (Chrome even pops up a dialog about page unresponsive).
In the analysis for the root cause of this structural slow performance I observe that the list contains multiple Lookup fields to other lists in the site, and also a 'Person or Group' field - which is a specific type of Lookup, to the hidden UserInformationList.
This multitude of Lookups in the list is the most significant cause for the slow performance. However, also on user experience / functional level, it is wrong designed: the usage of the page is to first load + display all the items from the list, and next the end-user must filter to select the relevant items.
I therefore first aligned with business owner on another functional approach: let the end-user start with specifying the relevant filter(s) [can be a combination of filters for multiple columns], and then select + retrieve only the items from the list that satisfy the filter-conditions. For the technical design, the Search tool must be setup future-proof, aka cloud ready. Thus interoperating with SharePoint either via CSOM or via REST services. I decided to utilize REST, so that data is returned in JSON data-format, and can be directly data-binded in client-side UI (I used Knockout.js; but same holds for other clientside UI frameworks as Angular, Ember).
In SharePoint REST one uses the $expand parameter to include referred lookup values in the result set. But similar as for XsltListView, this quickly destroys any performance:
So I needed a way to avoid the Lookup-expands, while still being able to filter on and select the values of the Lookup fields. Totally getting rid of the Lookup columns is not an option: for consistent data-management it is a Must-Have that one can select only from the values maintained in the Lookup list. And it holds even more for the Person field: selecting via the PeoplePicker ensures a user-friendly selection + validation.
The approach I decided to is to 'flatten' the lookup values in additional columns. Functional management can still manage the data-items using the lookup functionality, and on data-selection + retrieval I avoid the need to $expand.
The elements of the approach
  1. Per lookup value that is needed in the selection and/or display, add a single-line of text column to the list. Set them to hidden in the Item content type, so that their existense is invisible for functional management in the New/Edit/View list-forms;
      For the 'Person or Group' column, include 4 new fields, for
    1. Person Name,
    2. Photo,
    3. Department,
    4. and the UserId in the UserInformationList
  2. Create a SharePoint Designer Workflow on the list that activates on ItemCreated + ItemModified events. Design the workflow to propagate ('flatten') the lookup values to their respective flattened counter-field;
  3. Realize the bulk flattening of the existing list items through the same workflow, via javascript start the workflow on every item.
Impression of the solution setup + result

Friday, December 9, 2016

Freeze pane on SharePoint list with responsive scroll height

Earlier I blogged on how-to 'Freeze pane on SharePoint list': augment the standard SharePoint ListView UI to fixate the list header in visible sight when the user scrolls down the list in the browser. The user was totally happy with this delivered solution..., until opening the page on smaller screen (estate)... In the original code, the browser scrollbar is explicit hidden. An non-usability effect is that the user cannot scroll to the lower part in case the configured height of the 'scrollable listview' extends beyond the physical browser height. On the first coding, I made the explicit decision to hide the browser scrollbar via CSS, to avoid double scrollbars: without the CSS "overflow:hidden" property on 's4-workspace', the browser will always render its own scrollbar for pages that initial extend the browser height, even when later via clientside coding the page height is on-the-fly reduced to fit in browser height. The presence of double scrollbars - browser + on the list - is both confusing as non-esthetic. I prevented this via the 'overflow:hidden'.
However, in the initial code I didn't accompensate sufficient for smaller physical screens. I re-evaluated the 'freeze' code, and made some adjustments to make it responsive to the physical screen height. Another fix is to duplicate eventhandling on the standard listview header to the frozen header, for filtering, and 'select/deselect all' functionalities. Yet another fix is in the positioning of the ECB menu: default this is positioned relative to the listview top, which however itself is floating due the 'freeze' handling.
Updated code:
var visibleTableHeight = -1; // Freeze the header of listview. function FreezePane() { var $table = $(".ms-listviewtable").first().data("summary", "<listview name>"); var freezedTRHeight = 25; // Determine the available height for table to render in visible screen. if (visibleTableHeight === -1) { var origWorkspaceHeight = $("#s4-workspace").height(); var origTableHeight = $table.height(); var spaceInWorkspaceWithoutTable = origWorkspaceHeight - origTableHeight; var windowHeight = window.innerHeight; if (windowHeight == undefined) { windowHeight = document.documentElement.clientHeight; } var topPos = $("#s4-workspace").offset().top; var availableHeight = windowHeight - topPos - spaceInWorkspaceWithoutTable - freezedTRHeight; // Set visible height, with minimum of 300 visibleTableHeight = Math.max(availableHeight, 300); } // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { 'overflow-y': 'auto' } ); $table.wrap("
"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>").insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("TR.ms-viewheadertr:first", $table); var $firstRowTable = $("TR.ms-itmhover:first", $table); var $freezeHeader = $origHeader.clone(); // Propagate computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); var $alignerRow = $firstRowTable.clone(); $alignerRow.css( { 'visibility' : 'hidden' } ); $("#FreezedTR").append($alignerRow); $("#FreezedTR").wrap("<DIV style='HEIGHT: " + freezedTRHeight + "px;'></DIV>"); // Visual "hide" the orig header, make sure it still is rendered as otherwise the alignment of 'body' rows is altered $origHeader.children("th").each(function() { $(this).css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' , 'padding-top' : '0px', 'padding-bottom' : '0px' } ); $(this).find("div").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px', 'margin-top' : '0px', 'margin-bottom' : '0px' } ); $(this).find("input").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' } ); }); $origHeader.css( { 'max-height' : '1px', 'visibility' : 'hidden' } ); // Delegate eventhandlers from the copied+freezed header to the actual header of the listview. var $inputFH = $freezeHeader.find("input[title='Select or deselect all items']"); $inputFH[0].onclick = null; $inputFH[0].onfocus = null; $table.onmouseover = null; $inputFH.click(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('click'); }); $inputFH.focus(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('focus'); }); $("#FreezedTR").mouseover(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-listviewtable").trigger('mouseover'); }); ExecuteOrDelayUntilScriptLoaded(OverloadPositionCtxImg, "core.js"); Overload__doPostBack(); } // Need to update/overload positioning of 'ECB' icon as SharePoint standard it is relative positioned towards its direct parent; // and due scrolling would become invisible. function OverloadPositionCtxImg() { if ($.prototype.KI_base_PositionCtxImg === undefined) { $.prototype.KI_base_PositionCtxImg = PositionCtxImg; PositionCtxImg = function(c, b, h) { $.prototype.KI_base_PositionCtxImg(c, b, h); var a = c.style; a.top = (c.parentNode.offsetTop + b.clientTop) + "px"; }; } } // Overload MicrosoftAjax UpdatePanel function, to restore FreezePane after navigating to another page in the overview. function Overload_updatePanel() { if (Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel === undefined) { Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel = Sys.WebForms.PageRequestManager.prototype._updatePanel; Sys.WebForms.PageRequestManager.prototype._updatePanel = function(updatePanelElement, rendering) { Sys.WebForms.PageRequestManager.prototype.KI_base__updatePanel(updatePanelElement, rendering); if (updatePanelElement === $(".ms-listviewtable").first().data("summary", "<listview name>").closest("div")[0]) { if (("td.ms-addnew").length > 1) $("td.ms-addnew").last().closest("table").hide() FreezePane(); } }; } } function Overload__doPostBack() { if ($.prototype.KI_base__doPostBack === undefined) { $.prototype.KI_base__doPostBack = __doPostBack; __doPostBack = function (eventTarget, eventArgument) { // Make sure to overload the updatePanel function before postback from navigate button. if ($.prototype.KI_base__updatePanel === undefined) Overload_updatePanel(); $.prototype.KI_base__doPostBack(eventTarget, eventArgument); } } }

Thursday, April 21, 2016

Freeze pane on SharePoint list

In case of a larger SharePoint list, end-users may appreciate that the header row remains in the visible area while they scroll through the list. Out-of-the-box the SharePoint XsltListView webpart does not provide this functionality. However, it is possible to augment the standard behavior by modifying the rendered HTML through javascript. There are multiple examples published, but for some reason these did not immediate work for me. Did get me inspired though, and I coded a jQuery-snippet that when inserted in a page containing a XsltListView webpart delivers the 'freeze pane':
<script src="https://code.jquery.com/jquery-1.11.2.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { var $table = $(".ms-listviewtable").first().data("summary", "<listview name>"); // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { overflow: 'hidden' } ); $table.wrap("<DIV class='FreezedLV' style='OVERFLOW: auto; HEIGHT: 550px;'></DIV>"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>"). insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("TR.ms-viewheadertr:first", $table); var $firstRowTable = $("TR.ms-itmhover:first", $table); var $freezeHeader = $origHeader.clone(); // Propagate the computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); // Propagate the implicit width of row columns as explicit widths in first row to // avoid layout-changes when hiding the original header $firstRowTable.children("td").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($firstRowTable.children("td")[ownerIndex]).width(width + "px"); }); $("#FreezedTR").append($freezeHeader); $origHeader.hide(); }); </script>

Sunday, April 6, 2014

Asynchronous invocation of BCS

Return early from an elapse-time-expense BCS handling

Functional scenario: from UI context, check the user input via serverside business validation, and if functional correct propagate the same user input for processing in the business backend.
In this scenario, the user needs only to wait for the period of the validation step; if the outcome reports functional errors these must be reported to the user so (s)he is able to correct. In case of no errors, the subsequent business processing can be performed transparent to the user, no need to explicitly wait / block until this (potential time-expensive) processing is completed in the backend.
Technical context: HTML5/JavaScript client, that interopates with SAP business backend via a SharePoint RESTful services, which internally utilizes SharePoint BCS to invoke Duet Enterprise / Gateway services.

Inconvenient operating BCS from background thread

Initially, I thought it would be simple to realize the sketched execution context: handle the first step on the main thread of the SharePoint WCF REST service; if errors detected report these back; if no errors noticed, delegate the elapse-time expense propagation step to a background thread, and directly return from the main thread to unlock the client.
However, SharePoint / BCS reality proofed less trivial…
In my first attempt, I directly interoperate against BCS on the background thread:
main thread:
PerformCreation job = new PerformCreation() { _createMethod = createMethod, _jsonBody = jsonBody, _siteId = SPContext.Current.Site.ID, _userToken = SPContext.Current.Site.UserToken }; Thread backgroundThread = new Thread(new ThreadStart(job.PerformCreate)); backgroundThread.Start();  
background thread:
public void PerformCreate() { using (SPSite site = new SPSite(_siteId, _userToken)) { BCSModuleCall.Create(site, _createMethod, _jsonBody); } }  
When I executed this, the BCS Create invocation faults with a ‘Cannot complete this action’ exception. This orginates from the BCS internal method call ‘CalculatePermissionsForCurrentThread’, that checks whether in the context of the executing thread, the user has the rights to perform the BCS operation. Via code reverse engineering, I detected that the 'Microsoft.SharePoint.BusinessData.Infrastructure. BdcAccessControlList.AccessCheck(BdcRights rights)' method requires an active SPRequest context. And that only lives on the main / SharePoint thread.
To arrange that the BCS Create method is executed within the context of an active SPRequest, in my second attempt I changed the code to serverside issue from the background thread a webrequest to the SharePoint REST service. Although this technically works (that is, the ‘CalculatePermissionsForCurrentThread’ succeeds and the BCS Create operation is successful performed), a drawback is that you loose the credentials of the logged-on SharePoint user. It is not possible to propagate the claims-authenticated identity of the logged-on user to the webrequest invoked via WebClient class. It always authenticates and thus executes with the Application Pool identity.
public void PerformCreate() { var claimIdentity = (Microsoft.IdentityModel.Claims.ClaimsIdentity) Thread.CurrentPrincipal.Identity; var upnClaim = claimIdentity.Claims.FirstOrDefault( c => c.ClaimType.Equals(Microsoft.IdentityModel.Claims.ClaimTypes.Upn, StringComparison.InvariantCultureIgnoreCase)); if (upnClaim != null) { string upn = upnClaim.Value; WindowsIdentity windowsIdentity = Microsoft.IdentityModel.WindowsTokenService.S4UClient.UpnLogon(upn); var wic = windowsIdentity.Impersonate(); try { // SharePoint BCS API requires a current SPContext. In this thread, // there is no SPContext. To make sure the create is executed within // a current SPContext, create the entities by issuing REST request. WebClient webClient = new WebClient() { UseDefaultCredentials = true, BaseAddress = _baseAddress }; webClient.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=utf-8"); webClient.UploadDataAsync( (new Uri(String.Format("{0}/CreateItems", _baseAddress))), "POST", Encoding.UTF8.GetBytes(_jsonBody)); } finally { wic.Undo(); } } }  
In our scenario, it is a necessity that all data updates are authorized and audited for the issueing user. So although technical it is possible to delegate in this manner the BCS data update to a serverside background thread, from the perspective of data governance it is not allowed.

Solution: client-side operate BCS in asynchronous mode

Ultimately, I skipped the approach of serverside splitting up the BCS invocation in a synchronous blocking part, and an asynchronous non-blocking part, and instead decided to manage this from client-side. At first, issue a REST JSON request for the data validation; and let the user wait (block) on the response. If no validation errors detected, issue from the browser with the same JSON object the second update request in a fire-and-forget behaviour.
var checkUrl = bindingContext.$root.siteUrl + "/_vti_bin/WebApi/BCSAccessService.svc/CheckItems"; var checkCall = jQuery.ajax({ url: checkUrl, type: "POST", data: JSON.stringify(json), success: function (data) { if (Utilities.CheckNoErrorInResponse(data.BODY.ET_MESSAGES)) { var submitUrl = bindingContext.$root.siteUrl + "/_vti_bin/WebApi/BCSAccessService.svc/CreateItems"; // Data is already validated: //Fire-and-forget ajax async request and ignore response. var submitCall = jQuery.ajax({ url: submitUrl, type: "POST", data:JSON.stringify(json)}); alert("User input is validated and submitted."); window.parent.jQuery('#dialogForm').dialog('close'); } else { Utilities.DisplayResponseMessages(data.BODY.ET_MESSAGES); } } }).fail(Utilities.ErrorHandler);  
And this works out fine: the end-user earlier regains the input-control and responsiveness in the front-end application, while the update in the back-end is still processed in the background with the credentials of the logged-on user.

Sunday, August 18, 2013

Evaluating SharePoint Forum products

At one of the organizations I consult, there is a business demand for forum functionality in their external facing websites. The organization has selected SharePoint as target architecture for webapplications, including public websites. SharePoint as platform itself contains DiscussionBoard as a (kind of) forum functionality, but this is not qualified for usage on external facing websites. Among its criticisms are the look & feel which is very ‘SharePoint-like', and not what endusers expect and typically are familiar with on public websites. Also DiscussionBoard lacks forum functionalities of moderation, sticky posts, avatars, lock a post, rich text editing, tagging, rating, vote as answer. And an important restriction for the usability on public websites: a SharePoint DiscussionBoard in practice requires authenticated users (ok, you can allow anonymous access, but as posting topics and answers occurs via SharePoint forms you then will have to give anonymous users access to layouts folder. Not a wise decision to make from security perspective).
One option would be to custom develop forum functionality that satisfies the extended business requirements. But because we regard forum as commoditiy functionality, this is not something we want to develop and maintain ourselves. Therefore instead I did a market analysis and evaluation for available SharePoint forum products. It appears a very tiny market, with only 5 market products found:
  • TOZIT SharePoint Discussion Forum
  • Bamboo Solutions Discussion Board Plus for SharePoint
  • KWizCom Discussion Board feature
  • LightningTools Storm Forums
  • LightningTools Social Squared
In each product evaluation I addressed the following aspects:
  1. Product positioning by supplier (wide-scale internet usage, scaleability?)
  2. Installation of the product
  3. Effect of the installation on the SharePoint farm (assemblies, features, application pages, databases, …)
  4. Product documentation: installation manual, user / usage manual
  5. Functional Management: actions involved how-to provision a new forum
  6. Functional Usage: using a forum, in the roles of moderator and forum user
  7. User Experience and internet-ready
  8. Branding capabilities (CSS, clean HTML)
  9. Product support
For the requirement set of this customer, Social Squared turned out as the best fit. It has a rich forum feature set, on the same level as what you see in other (non-SharePoint) established internet forums. Also considered a strong point is that the forum administration is done in own SQL database(s), outside the SharePoint (content database). This allows to easily sharing a forum between multiple brands of the customer, each with their public presence in an own host-named site collection. And LightningTools is an well-known software products supplier, which gives the customer (IT) trust that the product will be adequatly and timely supported. As a demonstration of this: during my evaluation I already had extensive contact with their product support, answerring on questions and issues that I encountered, and also give me insight on the Social Squared roadmap.

Saturday, November 28, 2009

An in-between project: WPF screensaver for communicating our company values + news

A few weeks ago one of the TopForce founders asked me for an idea for communicating company messages to the TopForce employees. He was considering the usage of a screensaver for this. He had several requirements:
  1. It must be easily possible for the TopForce management to push new messages on-the-fly to our employees
  2. The displayed screensaver is attractive and draws on the attention of both the TopForce employee as coincidental passers-by
  3. The screensaver has a professional layout and styling
Being a SharePoint guy and fan, I immediately thought of applying our SharePoint based intranet for the management and distribution of the screensaver messages. Management can be done via a dashboard page containing multiple ListViews for the different categories of messages. Distribution can be done via SharePoint Lists.asmx webservice. So that's for addressing requirement 1. For the second requirement I considered WPF, you can do all kind of screen graphics and (dynamic) behaviour with that - from very simple to most advanced. For the last requirement; well I'm a developer, not a designer. Different roles, and especially different talents. I directly admit that I do not posses enough designer talent. But luckily I've several colleagues that are especially strong on the User eXperience area. Frodo Jansen did a very good job on desinging the screen-layout, addressing the third requirement.
The sketch of the software architecture for this mini application looks like this:
An impression of the end-result: