Unit Testing MDX

I was recently asked if it was possible to do some smoke testing on new deployments of SQL Server Analysis Services cubes. My initial answer was “yes, of course” – then someone asked me to actually do it. It turns out to be as easy as I thought it was but I wasn’t able to find any particular blog posts regarding how to do it so here goes. In order to perform unit tests against a cube, do the following:

  1. Create a new C# (or VB) Unit Test Project
  2. From the SQL Server 2014 (or SQL Server 2012 – but for SQL Server 2012 this is all built into Visual Studio) feature pack download page (http://www.microsoft.com/en-us/download/details.aspx?id=42295) select Download and select the 32 or 64 bit version of ADOMD.NET
  3. Add a reference to the Microsoft.AnalysisServices.AdomdClient.dll (located in C:Program FilesMicrosoft.NETADOMD.NET120)
  4. Add a reference to System.Data
  5. Import the namespace Microsoft.AnalysisServices.Adomd

You’re ready to go. The following is a simple unit test again the TFS Analysis cube:

  1: using System;
  2: using Microsoft.VisualStudio.TestTools.UnitTesting;
  3: using Microsoft.AnalysisServices.AdomdClient;
  4: using System.Diagnostics;
  5: 
  6: namespace dwUnitTests
  7: {
  8:     [TestClass]
  9:     public class UnitTest1
 10:     {
 11:         [TestMethod]
 12:         public void TestMethod1()
 13:         {
 14:             //Create the connection to the cube using integrated authentication
 15:             AdomdConnection connection = new AdomdConnection("Data Source=Olympia;Initial Catalog=Tfs_Analysis");
 16:             //Create the command and initialize it
 17:             AdomdCommand command = new AdomdCommand();
 18:                         
 19:             //Provide the MDX to retrieve data from the cube
 20:             command.CommandText = "SELECT NON EMPTY { [Measures].[Work Item Count] } ON COLUMNS, " 
 21:                 + "NON EMPTY { ([Work Item].[System_WorkItemType].[System_WorkItemType].ALLMEMBERS ) } ON ROWS "
 22:                 + "FROM [Team System]";
 23: 
 24:             //Set the connection for the command
 25:             command.Connection = connection;
 26: 
 27:             //Open a connection to the cube
 28:             connection.Open();
 29: 
 30:             //Retrieve the data
 31:             CellSet cs = command.ExecuteCellSet();
 32: 
 33:             //Close the connection
 34:             connection.Close();
 35: 
 36:             //Validate the data
 37:             Assert.IsTrue(cs.Axes[1].Positions[0].Members[0].Name == "[Work Item].[System_WorkItemType].&[Bug]");
 38:             Assert.IsTrue((int)cs.Cells[0].Value == 3);
 39:             Assert.IsTrue(cs.Axes[1].Positions[1].Members[0].Name == "[Work Item].[System_WorkItemType].&[Product Backlog Item]");
 40:             Assert.IsTrue((int)cs.Cells[1].Value == 1);
 41:             Assert.IsTrue(cs.Axes[1].Positions[2].Members[0].Name == "[Work Item].[System_WorkItemType].&[Task]");
 42:             Assert.IsTrue((int)cs.Cells[2].Value == 4);
 43:             Assert.IsTrue(cs.Axes[1].Positions[3].Members[0].Name == "[Work Item].[System_WorkItemType].&[Test Case]");
 44:             Assert.IsTrue((int)cs.Cells[3].Value == 2);
 45:         }
 46:     }
 47: }
 48: 

A couple of things to make life easier. I don’t write MDX that often and I can never remember the syntax when I have to. I had though that the most difficult part of this would be creating the select statement (line 20). It turns out you can do it without any work at all. To generate the MDX statement:

  • Open SQL Server Management Studio and connect to your cube
  • Browse the cube and create the dataset that you want returned as shown in Figure 1

image

Figure 1 – Browse view of an Analysis Services cube

  • Next, click the Design button on the right side of the toolbar to unselect it which results in the view shown in Figure 2

image

Figure 2 – MDX View to provide the result set shown in Figure 1

  • At this point, just copy the MDX, remove the formatting options (including and everything following the “Cell Properties”) and you’re done!

A word of caution on the Assert statements. I am using Axes and Positions to get the values here because I am using ExecuteCellSet to retrieve the data. There are other methods of retrieving the data so be careful when constructing the assert statements. It took me a bit of trial and error to be able to drill through the object model in debug mode to figure out what I should be testing.

And that’s it – you can now perform quality checks on the results of a cube to ensure that the cube is processing correctly. This probably does not apply to very many people but for those that it does apply to, using proper software development techniques for cube development is every bit as important as using these techniques elsewhere. Maybe more so because end users rely on data from cubes to drive business decisions – if this data is wrong…

Activating Azure without Credit Card utilizing MSDN Subscription

The wait is finally over. Microsoft Azure is generally available in Bangladesh now. You can either try for free trial for one month entitling to $200 Azure credit, but you’d require an international credit/debit card OR dollar endorsed to your local credit card for foreign travel to activate that although you will not be charged until the period is expired or you’ve overspent that $200. However, if credit card is a hassle for you, you have a great option of MSDN Subscription.

If you/your company already has MSDN Subscription OR you’re a Microsoft Student Partner (MSP), you can activate your monthly up to $150 Azure benefit by following simple steps:

1

  • You will be navigated to the following page. Fill in the required information. Once you have completed the mobile verification in Step 2, the sign up button will be activated. Click on “Sign up”

1

  • After clicking on the “Sign up” button you will be navigated to the following page. Wait and refresh at intervals until the subscription is activated. The status will be changed from “Pending” to “Active” when the subscription becomes active.

4

  • Next click on the active subscription – “Visual Studio Ultimate with MSDN”

5

You can use this portal:

  • to set up new services, resources and components
  • to get immediate control of your services, resources and components.
  • to get notifications

Congratulations, your Azure account is all set up and good to go. You can now start creating websites, virtual machines, mobile services, cloud services, SQL Databases and a lot more with Microsoft Azure.

Article courtesy: Anika Sayara

The Start of my Microsoft Dynamics Marketing blog

Hi,

It is really overdue time that I start blogging on Microsoft Dynamics Marketing. I am looking forward to start sharing about various topics.

You can find general product documentation information here:

http://www.microsoft.com/en-us/dynamics/crm-marketing.aspx

Or follow my Curah!’s here (Link collections with essential information and materials):

My Curah!s on Microsoft Dynamics Marketing

 

Christian Abeln
Senior Program Manager
Microsoft Dynamics Marketing

Just Collecting Dust…

It’s Edinburgh Festival time again, and as usual they voted a winning joke. One of my favourite actors and stand-up comedians, Tim Vine, won it for the second time this year with a new one-liner: “I’m thinking of selling my vacuum cleaner. It’s just collecting dust.”

…(read more)

AppV 5: Important Considerations and Ramifications for Package Upgrade and VFS Write Mode

If you are running any version of the App-V 5 client or Sequencer prior to App-V 5.0 Service Pack 2 Hotfix 4 – stop reading. This does not apply to your environment. If you are running HF4 or sooner, you need to have a good understanding of the net effects of toggling VFS Mode on and/or off during package upgrade.
VFS Write Mode was introduced to (my words) “fix bad brake jobs” when it comes to application development. Applications that need to be able to read and write configurations…(read more)

Microsoft Azure SQL Database Basic, Standard ? Premium ????

2014年4月宣布了新的 Microsoft Azure SQL Database 服務來取代既有的 Microsoft SQL Database Business/Web Edition。2014 年 8 月 26 日 SQL Server 產品主管 Eron Kelly 宣布新版本 Microsoft SQL Database Basic, Standard 與 Premium 版即將於 2014 年 9 月脫離技術預覽階段,開始正式營運 ( http://azure.microsoft.com/blog/2014/08/26/new-azure-sql-database-service-tiers-generally-available-in-september-with-reduced-pricing-and-enhanced-sla/ ),新的雲端資料庫服務,與過去版本相較有了以下的改善 :

  • 不停機服務水準 (SLA) 由 99.9% 提升為 99.99 %
  • 單一資料庫容量上限提高
  • 較可預期的執行效能
  • 用戶可以自行回存資料 (Self-service restore) ,依據不同等級版本可回溯資料庫時間從 7-35 天不等
  • 以小時為單位計價
  • 高階版本提供跨資料中心災難備援機制

與傳統 Microsoft SQL Server 規劃相較,客戶 Microsoft Azure SQL Database 不同等級的選用,可以參考 http://msdn.microsoft.com/library/azure/dn369873.aspx ,在此節錄最重要的表格, 新公布的 Standard/S0 在 2014/8/30 時尚未更新進此一表格,先就已經公布的 DTU 數據做些調整。

Azure SQL Database 等級

Database Throughput Units (DTUs)

單一資料庫容量上限 (GB)

Worker Threads 上限

Sessions 數上限

預期效能

Basic 5 2 20 100
Standard/S0 10 250 待確認 待確認 較好
Standard/S1 20 250 50 200 較好
Standard/S2 50 250 100 500 較好
Premium/P1 100 500 200 2,000 最佳
Premium/P2 200 500 400 4,000 最佳
Premium/P3 800 500 1,600 16,000 最佳

 

資料庫庫吞吐量單元(Database Throughput Unit ,DTU):這是一個綜合多項能力的單位,結合了 CPU,記憶體,資料讀寫能力成為一個單位。 理論上 5 DTU 的效能水準比 1 DTU 要多五倍,Worker thread 在邏輯上表示 Microsoft Azure SQL Database 允許的執行緒數量上限,可以視為是作業系統允許的執行緒數量上限,隱身在資料庫服務背後 ;Worker thread 默默地執行資料庫服務所指派的工作。而 Sessions 數則是指邏輯上伺服器端與用戶端所建立能夠交換資料的單位,Session 數實際上並不等同於實體上網路 Connection 連線數,但兩者間數量差異不大,可以視為是能夠允許的網路連線數量。雲端服務的特質在於資源共享,資源共享也意味著必須限制單一用戶的用量,以避免其他租戶使用時受到影響,因此在資料庫規劃上需要隨時注意相關資訊。

Expiring External User Sharing in SharePoint Online

SharePoint Online makes it extremely easy to share sites and content with external users. For this reason, SharePoint Online has seen rapid adoption for many extranet scenarios and in OneDrive for Business. SharePoint Online provides administrators the tools to manage external sharing, including enabling/disabling sharing and visibility into external users within a site collection. External sharing is simple, secure, and extremely powerful.  However, once content is shared externally, it stays shared forever…or at least until it is manually revoked by a content owner or administrator. In this post, I will outline a solution to set expiration timers on external sharing in SharePoint Online. The solution will also give content owners easy methods to extend/revoke external user access. This layer of external sharing governance is frequently requested by my customers and easily achievable with the Office 365 Developer platform. Here is comprehensive video overview of the solution if you want to see it in action:

(Please visit the site to view this video)

 

NOTE: Although this solution is exclusive to SharePoint Online, external sharing can be delivered in a similar way on-premises. That said, Microsoft has pulled off some crazy technical gymnastics in SharePoint Online to make it effortless for users to share and IT to manage (read: don’t try this at home kids). If you really want to deliver this on-premises, I highly recommend investigating CloudExtra.net for on-premises “Extranet in a box” with SharePoint.

 

The Solution

The solution logic will be based expiration and warning thresholds. These thresholds could be tenant-wide, template-specific, site-specific, and almost anything in between. The expiration threshold represents the number of days external users will be permitted access before their access expires or is extended by a content owner. The warning threshold represents the number of days external users will be permitted access before the solution sends expiration warnings to the “invited by” content owner or site collection administrator. These email warnings will provide commands to extend the external share or immediately revoke access. If the warnings are ignored long enough to reach the expiration threshold, the external access will automatically be revoked by the solution. This “NAG” feature is very similar to Microsoft’s implementation of site disposition…just in this case we are talking external access disposition. Don’t follow? Here is a quick 50sec cartoon that simplifies the concept:

(Please visit the site to view this video)

 

The solution is implemented with three components. First, a console application “timer job” will run daily to iterate site collections, capture all external users in a site, and process shares that exceed thresholds (either sending email warnings or revoking access). A database will keep track of all external users by site collection, including the original shared to date and the date their access was extended (if applicable). Finally, a website will provide content owners and site administrators an interface to respond to expiration warnings by extending or revoking external access for specific external users. The solution structure in Visual Studio can be seen below and illustrates the projects outlined above. The entire solution could be deployed to a single free Azure Website (with WebJob) and SQL Azure Database.

Detail of Solution in Visual Studio

 

Finding Detailed Information on External Sharing

The first challenge in building this solution was finding accurate external sharing details…at minimum the external user identity, invited by user identity, and shared date. This turned out to be surprisingly challenging. I started by looking at the “Access Requests” list that exists in site collections that have external sharing. This stores all outstanding and historical external sharing invitations…or so I thought. It turns out this will only track external users that haven’t already accepted a sharing request somewhere else in the entire tenant. For example…if I share “Site Collection A” with “Joe Vendor” and “Joe Vendor” is already an active external user somewhere else in the tenant (ex: “Site Collection B”), he will never show up in the “Access Requests” list.

The Office 365 Administration Portal offers an External Sharing menu that enables administrators to manually view/manage external users by site collection. If these details were exposed in the user interface, I held hope a matching API would exist in the SharePoint Online Administration assemblies. Turns out, I was right…the Microsoft.Online.SharePoint.TenantManagement namespace has an Office365Tenant class with GetExternalUsersForSite method (tip: be care not to confuse this Office365Tenant class with the slightly different Tenant class used for site collection provisioning…they are even in slight different namespaces of the assembly). The GetExternalUsersForSite method takes a site collection URL and is paged to return 50 external users at a time. I used the code below to convert ALL the external users into my own entities so I could quickly dispose the administration client context:

GetExternalUsersForSite

//use O365 Tenant Administration to get all the external sharing details for this site
List<ExternalShareDetails> shares = new List<ExternalShareDetails>();
string adminRealm = TokenHelper.GetRealmFromTargetUrl(tenantAdminUri);
var adminToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, tenantAdminUri.Authority, adminRealm).AccessToken;
using (var clientContext = TokenHelper.GetClientContextWithAccessToken(tenantAdminUri.ToString(), adminToken))
{
    //load the tenant
    var tenant = new Office365Tenant(clientContext);
    clientContext.Load(tenant);
    clientContext.ExecuteQuery();

    //initalize varables to going through the paged results
    int position = 0;
    bool hasMore = true;
    while (hasMore)
    {
        //get external users 50 at a time (this is the limit and why we are paging)
        var externalUsers = tenant.GetExternalUsersForSite(siteUrl, position, 50, String.Empty, SortOrder.Descending);
        clientContext.Load(externalUsers, i => i.TotalUserCount);
        clientContext.Load(externalUsers, i => i.ExternalUserCollection);
        clientContext.ExecuteQuery();

        //convert each external user to our own entity
        foreach (var extUser in externalUsers.ExternalUserCollection)
        {
            position++;
            shares.Add(new ExternalShareDetails()
            {
                AcceptedAs = extUser.AcceptedAs.ToLower(),
                DisplayName = extUser.DisplayName,
                InvitedAs = extUser.InvitedAs.ToLower(),
                InvitedBy = (String.IsNullOrEmpty(extUser.InvitedBy)) ? null : extUser.InvitedBy.ToLower(),
                UserId = extUser.UserId,
                WhenCreated = extUser.WhenCreated
            });
        }
                       
        //determine if we have more pages to process
        hasMore = (externalUsers.TotalUserCount > position);
    }
}

 

Here are the details of what GetExternalUsersForSite returns for each external user:

Property Description
AcceptedAs The email address used to accept the external share
DisplayName The display name resolved when the user accepts the external share
InvitedAs The email address that was provided to share externally
InvitedBy The email address of the user that invited the external user**
UniqueId A 16-character hexadecimal unique id for the user (ex: 1003BFFD8883C6D1)
UserId User ID of the external user in the SiteUsers list for the site collection in question
WhenCreated The date the external user was first resolved in the tenant***

**InvitedBy will only contain a value if the share introduced the external user to the tenancy (ie – their first accepted invite to the tenant)

***WhenCreated returns the date the external user was first resolved in the tenant…NOT the shared date

The results from GetExternalUsersForSite provided a comprehensive list of external users for a site collection, but had a data quality issue for external users that had previously accepted external sharing requests somewhere else in my tenant (such as “Joe Vendor” mentioned earlier). For these users, the InvitedBy was empty and the WhenCreated date represented the date they first accepted an external share in my tenant (not when they accepted sharing for that specific site collection). InvitedBy isn’t that critical as I can warn the site administrator, but the original share date is essential for the expiration logic of the solution. I found an accurate date in an old friend…the user information list for the site collection (ex: _catalogs/users). This list is accessible via REST and very easy to query since GetExternalUsersForSite gives us the actual UserId of the user within the site collection. We can use the Created column to determine the accurate share date.

Using REST w/ User Information List for Actual Share Date
var shareRecord = entities.ExternalShares.FirstOrDefault(i => i.LoginName.Equals(externalShare.AcceptedAs));
if (shareRecord != null)
{
    //Update LastProcessedDate column of the record with the processDate
    shareRecord.LastProcessedDate = processDate;
    entities.SaveChanges();
}
else
{
    //get the original share date
    var details = getREST(accessToken, String.Format(“{0}/_api/Web/SiteUserInfoList/Items({1})/FieldValuesAsText”, siteUrl, externalShare.UserId));
    externalShare.WhenCreated = Convert.ToDateTime(details.Descendants(ns + “Created”).FirstOrDefault().Value);
    shareRecord = new ExternalShare()
    {
        UniqueIdentifier = Guid.NewGuid(),
        SiteCollectionUrl = siteUrl.ToLower(),
        LoginName = externalShare.AcceptedAs,
        UserId = externalShare.UserId,
        InvitedBy = (String.IsNullOrEmpty(externalShare.InvitedBy)) ? siteOwner.Email : externalShare.InvitedBy,
        OriginalSharedDate = externalShare.WhenCreated,
        LastProcessedDate = processDate
    };
    entities.ExternalShares.Add(shareRecord);
    entities.SaveChanges();
}

 

The definitive source of external user information is collected from a combination of the GetExternalUsersForSite method AND the User Information List. The table below summarizes the sourcing.

  New External Users in Tenant Existing External Users in Tenant
External User Identity GetExternalUsersForSite GetExternalUsersForSite
Invited By User Identity GetExternalUsersForSite /_api/site/Owner
Shared Date /_api/Web/SiteUserInfoList /_api/Web/SiteUserInfoList

 

Revoking and Extending Access

The Office365Tenant class has a RemoveExternalUser class, which takes an array of unique external user ids. However, this doesn’t allow you to specify a site collection so I suspect it removes the external user from the entire tenant (which we don’t want). Even if this method was site collection specific, I think it is good practice to minimize the use of the SharePoint Online Administration assembly whenever possible. In this case, GetExternalUsersForSite provided a site-specific UserId for external users, which can be used to remove them from the SiteUsers collection in the root web. This will cascade delete the external user everywhere in the site collection. Doing this could leave broken permission inheritance in the site. I originally had heartburn over this, but broken inheritance seems to be an accepted reality of the new open sharing user experience. Also notice that RefreshShareDate takes precedence over OriginalShareDate…this is how we take extending access into consideration (RefreshShareDate will be null for any external user that hasn’t been extended).

Revoke External User by Deleting SiteUser

//check if the record falls inside the warnings
double daysActive = processDate.Subtract(shareRecord.OriginalSharedDate).TotalDays;
if (shareRecord.RefreshSharedDate != null)
    daysActive = processDate.Subtract((DateTime)shareRecord.RefreshSharedDate).TotalDays;

//check for cutoff
if (daysActive > cutoffDuration)
{
    //remove the SPUser from the site
    clientContext.Web.SiteUsers.RemoveById(externalShare.UserId);
    clientContext.ExecuteQuery();

    //delete the record
    entities.ExternalShares.Remove(shareRecord);
    entities.SaveChanges();
}

 

To extend access, we will allow content owners and site collection administrators to reset the expiration clock through an MVC web application. Below you can see the code used to send expiration warnings (which contain direct links to extend/revoke views). Notice that the solution leverages GUIDs to provide direct links to controller actions.

Sending Expiration Warnings
else if (daysActive > warningDuration)
{
    int expiresIn = Convert.ToInt32(cutoffDuration – daysActive);
    //send email to InvitedBy (which will be site collection owner when null)
    EmailProperties email = new EmailProperties();
    email.To = new List<String>() { shareRecord.InvitedBy };
    email.Subject = String.Format(“Action Required: External sharing with {0} about to expire”, externalShare.AcceptedAs);
    email.Body = String.Format(“<html><body><p>You are receiving this message because you are the site administrator of <a href=’{0}’>{0}</a> OR you shared it with {1}. The external access for this user is set to expire in {2} days. Use the link below to view additional details and perform actions to revoke OR extend access for another {3} days. If you do not act on this notice, the external access for this user to terminate in {2} days.</p><ul><li><a href=’{4}Details/{5}’>View Details</a></li><li><a href=’{4}Extend/{5}’>Extend {3} Days</a></li><li><a href=’{4}Revoke/{5}’>Revoke Access</a></li></ul></body></html>”, siteUrl, externalShare.AcceptedAs, expiresIn.ToString(), cutoffDuration.ToString(), webUrl, shareRecord.UniqueIdentifier);
    Utility.SendEmail(clientContext, email);
    clientContext.ExecuteQuery();
}

 

Expiration Warning Email

Below is the MVC controller for both Extend and Revoke. Revoke in the controller is identical to the “timer job” console application and Extend simply sets the RefreshShareDate.

MVC Controller

// GET: Details/92128104-7BA4-4FEE-BB6C-91CCE968F4DD
public ActionResult Details(string id)
{
    if (id == null)
    {
        return View(“Error”);
    }
    Guid uniqueID;
    try
    {
        uniqueID = new Guid(id);
    }
    catch (Exception)
    {
        return View(“Error”);
    }
    ExternalShare externalShare = db.ExternalShares.FirstOrDefault(i => i.UniqueIdentifier == uniqueID);
    if (externalShare == null)
    {
        return View(“Error”);
    }
    return View(externalShare);
}

// GET: Extend/92128104-7BA4-4FEE-BB6C-91CCE968F4DD
public ActionResult Extend(string id)
{
    if (id == null)
    {
        return View(“Error”);
    }
    Guid uniqueID;
    try
    {
        uniqueID = new Guid(id);
    }
    catch (Exception)
    {
        return View(“Error”);
    }
    ExternalShare externalShare = db.ExternalShares.FirstOrDefault(i => i.UniqueIdentifier == uniqueID);
    if (externalShare == null)
    {
        return View(“Error”);
    }

    //update the share with a new RefreshSharedDate
    externalShare.RefreshSharedDate = DateTime.Now;
    db.SaveChanges();

    return View(externalShare);
}

// GET: Revoke/92128104-7BA4-4FEE-BB6C-91CCE968F4DD
public ActionResult Revoke(string id)
{
    if (id == null)
    {
        return View(“Error”);
    }
    Guid uniqueID;
    try
    {
        uniqueID = new Guid(id);
    }
    catch (Exception)
    {
        return View(“Error”);
    }
    ExternalShare externalShare = db.ExternalShares.FirstOrDefault(i => i.UniqueIdentifier == uniqueID);
    if (externalShare == null)
    {
        return View(“Error”);
    }

    //get an AppOnly accessToken and clientContext for the site collection
    Uri siteUri = new Uri(externalShare.SiteCollectionUrl);
    string realm = TokenHelper.GetRealmFromTargetUrl(siteUri);
    string accessToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, siteUri.Authority, realm).AccessToken;
    using (var clientContext = TokenHelper.GetClientContextWithAccessToken(siteUri.ToString(), accessToken))
    {
        //remove the SPUser from the site
        clientContext.Web.SiteUsers.RemoveById(externalShare.UserId);
        clientContext.ExecuteQuery();

        //delete the record
        db.ExternalShares.Remove(externalShare);
        db.SaveChanges();
    }

    //display the confirmation
    return View(externalShare);
}

 

Warning Detail in MVC App

Revoke Confirmation in MVC App

Extend Confirmation in MVC App

You might be wondering where those important warning and expiration thresholds are configured. For simplicity in this solution, I configured them in the appSettings section of the console app and MVC app configuration files. However, the solution could be configured to implement more advanced threshold logic such as template-specific thresholds.

Configuration appSettings
  <appSettings>
    <add key=ClientIDvalue=YOUR_APP_ID/>
    <add key=ClientSecretvalue=YOUR_APP_SECRET/>
    <add key=WarningDurationvalue=50/>
    <add key=CutoffDurationvalue=60/>
    <add key=TenantNamevalue=rzna/>
    <add key=TenantUpnDomainvalue=rzna.onmicrosoft.com/>
  </appSettings>

 

Final Thoughts

I hope this solution helped illustrate how Office 365 can deliver great sharing user experience WITH the sharing compliance/governance that legal teams and information security are demanding. You can download the entire solution on the Office 365 Developer Patterns and Practices (PnP) site of GitHub and view deployment details HERE

Small Basic – Transparent GraphicsWindow

I recently added a feature to the LitDev Extension to create a completely transparent GraphicsWindow.

The method to do this is:

LDUtilities.TransparentGW()

I can’t show a screenshot because there is absolutely nothing to see!

So, what’s the point…

Well, anything we add to the transparent GraphicsWindow will be visible, so we can do things like:

  • Create a window with a non-rectangular shape by drawing a png image.
  • Create nice simple widget applications.
  • Make things appear to float free from a containing window.

Features

  • The transparent GraphicsWindow must be created before any other command creates a GraphicsWindow.
  • Once a transparent GraphicsWindow is created it cannot be changed back to a normal GraphicsWindow.
  • Because the window is completely transparent, it doesn’t even register Mouse Clicks or other events unless something is drawn on it or its background is modified with GraphicsWindow.BackgroundColor.
  • A partially transparent color can be created using the hex format with the first 2 characters being opacity (e.g. “#01FFFFFF” is almost completely transparent white – you won’t see it but it will register events if used as a background).
  • You can add anything to the transparent GraphicsWindow that you would to a normal one, including drawings, shapes and controls as well as move, rotate, show/hide and zoom (everything you can do normally).
  • You can use the associated new method LDUtilities.TopMostGW(“True”) to ensure that the window always remains above all other windows, good for widgets.

Example

The following is the code for a simple clock widget using these methods.

'Simple LitDev Extension widget clock
 
'Transparent Topmost GraphicsWindow
LDUtilities.TransparentGW()
LDUtilities.TopMostGW("True")
 
'Clock Face
GraphicsWindow.Width = 100
GraphicsWindow.Height = 100
GraphicsWindow.BrushColor = "#40FFFFFF" 'Partially transparent White
GraphicsWindow.FillEllipse(0,0,100,100)
 
GraphicsWindow.FontSize = 10
For i = 1 To 12
  angle = i*Math.PI/6 - Math.Pi/2
  GraphicsWindow.DrawText(50+45*Math.Cos(angle)-3,50+45*Math.Sin(angle)-7,i)
EndFor
 
'Hands
GraphicsWindow.PenColor = "Black"
GraphicsWindow.PenWidth = 4
hourHand = Shapes.AddLine(0,0,0,0)
GraphicsWindow.PenWidth = 2
minuteHand = Shapes.AddLine(0,0,0,0)
GraphicsWindow.PenColor = "Red"
GraphicsWindow.PenWidth = 1
secondHand = Shapes.AddLine(0,0,0,0)
 
'Register Events
GraphicsWindow.MouseDown = OnMouseDown
GraphicsWindow.MouseUp = OnMouseUp
LDDialogs.AddRightClickMenu("1=Exit;","")
LDDialogs.RightClickMenu = OnRightClickMenu
 
'MAIN LOOP
 
While ("True")
  'Get angles (Clockwise from top)
  second = Clock.Second*Math.PI/30 - Math.Pi/2
  minute = (Clock.Minute+Clock.Second/60)*Math.PI/30 - Math.Pi/2
  hour = (Clock.Hour+Clock.Minute/60+Clock.Second/3600)*Math.PI/6 - Math.Pi/2
  
  'Move hands - extension used to move lines coz its easier
  LDShapes.MoveLine(hourHand,50,50,50+35*Math.Cos(hour),50+35*Math.Sin(hour))
  LDShapes.MoveLine(minuteHand,50,50,50+45*Math.Cos(minute),50+45*Math.Sin(minute))
  LDShapes.MoveLine(secondHand,50,50,50+50*Math.Cos(second),50+50*Math.Sin(second))
  
  'Move the clock with mouse down
  If (mouseDown) Then
    GraphicsWindow.Left = offsetX+Mouse.MouseX
    GraphicsWindow.Top = offsetY+Mouse.MouseY
  EndIf
  
  Program.Delay(100) ' Delay 0.1 sec to prevent mashing cpu unnecessarily
EndWhile
 
'EVENT HANDLING SUBROUTINES
 
Sub OnMouseDown
  mouseDown = "True"
  offsetX = GraphicsWindow.Left-Mouse.MouseX
  offsetY = GraphicsWindow.Top-Mouse.MouseY
EndSub
 
Sub OnMouseUp
  mouseDown = "False"
EndSub
 
Sub OnRightClickMenu
  If (LDDialogs.LastRightClickMenuItem = 1) Then
    Program.End()
  EndIf
EndSub

The partially transparent clock floats above all windows, it can be moved by grabbing with a mouse click and dragging.  A right click gives an option to exit it.

With these basic code segments you can create your own fancy professional looking widgets, perhaps with options to change and store settings.

Some code patterns I don't love in C#.

Pattern 1: TryGetFoo that returns boolean.

MyEnum ret;
if (Enum.TryParse<MyEnum>(normalized, true, out ret)) { return ret; } else { return null; }

You would think this slightly more concise line would work

MyEnum ret;
return Enum.TryParse<MyEnum>(str, true, out ret) ? ret : null;

But it turns out that is no good. Compiling the ternary operator, compiler can’t even figure out the common type of null and MyEnum is Nullable<MyEnum>!

Either way, what’s not to love? Having to declare a throwaway out parameter. Having to explicitly marshal boolean true into value, and boolean false into null. And enums themself. Which are not nullable. Luckily nullable enums (MyEnum?) come to the rescue by being nullable… but they are a different type, so you have to think about which to use when.

If javascript had enums it would probably be like:

return enumType.TryParse(str, true);

Of course javascript doesn’t really have enums, so the function might either be returning some object, some string, or some number. But the point of parsing here is that at least you know it’s constrained to a set of certain fixed values.
 

Pattern 2: is/as/casting.

if (reader.Value is Foo)
{
(reader.Value as Foo).doSomething();
}

If only the compiler and intellisense could auto-infer the Fooness in this scenario… of course there are reasons to do with method overloading that this would sometimes realy suck too.
Speaking of method overloading…

Pattern 3: many slightly different method overloads with optional parameters

LogException(a, b = null, c = null, d = null)
LogException(a, e, f = null)

The thing I don’t like about this pattern is that it always turns out that the particular parameters you want to use are not quite matching up with the order/selection of parameters that someone else thought would make good defaults.
To me it’s awesome how this one gets solved in javascript. Parameter objects to the rescue!
LogException({ a: x, b: y, f: z});
Now you no longer have artificial strictures on which orders and subsets of parameters are valid to supply. Only the function logic itself will govern this (by throwing if it really must)
Of course for that to work, someone had to try hard to make one function work for all possible parameter sets. But at least there’s much saved stress in the process of consumption. :-P

Now that I’ve written this rant, happy coincidence! It may have jogged my mind towards figuring out a little bug I was having related to enums and nullability. I put [JsonConverter] attribute on an enum type MaxMemoryPolicy. However, when my properties are of type [MaxMemoryPolicy?] I don’t think the converter was getting called in the serialize string to nullable enum direction – hey, type mismatch! Let’s just convert it to null and see if we can assign that… Yup? Cool! Arggggg.
Now some debugging will confirm or deny this new hypothesis… denied. OK it was something else. But I had to consider the possibility. :p