WIP, psts in place, parsing mostly correclty
This commit is contained in:
@@ -6,5 +6,15 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/* Your site config here */
|
/* Your site config here */
|
||||||
plugins: [`gatsby-plugin-sass`],
|
plugins: [
|
||||||
|
`gatsby-plugin-sass`,
|
||||||
|
{
|
||||||
|
resolve: `gatsby-source-filesystem`,
|
||||||
|
options: {
|
||||||
|
name: `posts`,
|
||||||
|
path: `${__dirname}/src/posts`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`gatsby-transformer-remark`
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
43
gatsby-node.js
Normal file
43
gatsby-node.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const path = require(`path`);
|
||||||
|
const { createFilePath } = require(`gatsby-source-filesystem`);
|
||||||
|
|
||||||
|
exports.onCreateNode = ({ node, getNode, actions }) => {
|
||||||
|
const { createNodeField } = actions;
|
||||||
|
|
||||||
|
if (node.internal.type === `MarkdownRemark`) {
|
||||||
|
var slug = createFilePath({ node, getNode, basePath: `pages` });
|
||||||
|
createNodeField({
|
||||||
|
node,
|
||||||
|
name: `slug`,
|
||||||
|
value: slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createPages = async ({ graphql, actions }) => {
|
||||||
|
const { createPage } = actions;
|
||||||
|
|
||||||
|
const result = await graphql(`
|
||||||
|
query {
|
||||||
|
allMarkdownRemark {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
|
||||||
|
createPage({
|
||||||
|
path: node.fields.slug,
|
||||||
|
component: path.resolve(`./src/templates/post.js`),
|
||||||
|
context: {
|
||||||
|
slug: node.fields.slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
9
gatsby-ssr.js
Normal file
9
gatsby-ssr.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const onRenderBody = ({ setPostBodyComponents }) => {
|
||||||
|
setPostBodyComponents([
|
||||||
|
<script src="https://platform.linkedin.com/badges/js/profile.js" async defer></script>,
|
||||||
|
<script>var clicky_site_ids = clicky_site_ids || []; clicky_site_ids.push(101254860);</script>,
|
||||||
|
<script src="//static.getclicky.com/js" async defer></script>
|
||||||
|
])
|
||||||
|
}
|
||||||
847
package-lock.json
generated
847
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,11 @@
|
|||||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
|
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"disqus-react": "^1.0.10",
|
||||||
"gatsby": "^2.24.61",
|
"gatsby": "^2.24.61",
|
||||||
"gatsby-plugin-sass": "^2.3.13",
|
"gatsby-plugin-sass": "^2.3.13",
|
||||||
|
"gatsby-source-filesystem": "^2.3.30",
|
||||||
|
"gatsby-transformer-remark": "^2.8.35",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0"
|
"react-dom": "^16.12.0"
|
||||||
|
|||||||
22
src/components/footer.js
Normal file
22
src/components/footer.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const date = new Date();
|
||||||
|
const timestamp = date.toISOString();
|
||||||
|
const year = date.getYear() + 1900;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<small>
|
||||||
|
© <time datetime={timestamp}>{year}</time>.
|
||||||
|
All rights reserved.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<hr />
|
||||||
|
<div class="LI-profile-badge" data-version="v1" data-size="medium" data-locale="en_US" data-type="horizontal" data-theme="dark" data-vanity="benjaminstevenramey"><a class="LI-simple-link" href='https://www.linkedin.com/in/benjaminstevenramey?trk=profile-badge'>Benjamin Ramey</a></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,17 +5,16 @@ export default function Header() {
|
|||||||
<header>
|
<header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex justify-content-between align-item-center">
|
<div class="d-flex justify-content-between align-item-center">
|
||||||
<a href="site.baseurl/" class="d-flex justify-content-start align-items-center mr-2" title="Home">
|
<a href="/" class="d-flex justify-content-start align-items-center mr-2" title="Home">
|
||||||
<img src="https://en.gravatar.com/userimage/738990/32d8e61848233a67f5e02dcc43be0ff8.jpeg"
|
<img src="https://en.gravatar.com/userimage/738990/32d8e61848233a67f5e02dcc43be0ff8.jpeg"
|
||||||
alt="Gravatar" />
|
alt="Gravatar" />
|
||||||
<span class="ml-2">site.title</span>
|
<span class="ml-2">Ben Ramey's Blog</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="d-none d-sm-flex flex-column justify-content-around ml-2">
|
<div class="d-none d-sm-flex flex-column justify-content-around ml-2">
|
||||||
<small>site.tagline</small>
|
<small>Scripture, programming problems, solutions and stories.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
|
import Footer from './footer';
|
||||||
|
import Nav from './nav';
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
return (
|
return (
|
||||||
<body>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
|
<Nav />
|
||||||
|
<main>
|
||||||
|
<div className="container">
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
17
src/components/nav.js
Normal file
17
src/components/nav.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Nav() {
|
||||||
|
return (
|
||||||
|
<nav class="main">
|
||||||
|
<div class="container d-flex justify-content-center">
|
||||||
|
{/* {% for page in site.pages %}
|
||||||
|
{% if page.title != null %}
|
||||||
|
{% if page.layout == "page" %} */}
|
||||||
|
<a href="page.url | relative_url">page.title</a>
|
||||||
|
{/* {% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %} */}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Layout from "../components/layout"
|
import Page from "../templates/page";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Page>
|
||||||
<div>hello world!</div>
|
<div>hello world!</div>
|
||||||
</Layout>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Replacing newline characters in VB.NET strings"
|
||||||
|
date: 2012-06-12
|
||||||
|
description: "Replacing newline characters in VB.NET strings"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [vb.net]
|
||||||
|
---
|
||||||
|
I’m not very familiar (which I am actually very happy for!) with VB.NET and so I had a hard time figuring out how to trim newline characters from a string in a Reporting Services report I was working on.
|
||||||
|
|
||||||
|
Turns out none of the “regular” methods really work. The Trim function only removes spaces and anything like Replace(“string”, “\n”, “”) didn’t work.
|
||||||
|
|
||||||
|
You have to use the Chr(13) or Chr(10) functions to actually get the character values for the newline characters. So, my solution looked like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
Replace(Replace(stringVariable, Chr(10), “”), Chr(13), “”)
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Thanks to this forum post for providing the clues:
|
||||||
|
[http://www.daniweb.com/forums/thread54797.html](http://www.daniweb.com/forums/thread54797.html)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Converting a Hexadecimal Color to a System.Drawing.Color Object"
|
||||||
|
date: 2009-09-24
|
||||||
|
description: "Converting a Hexadecimal Color to a System.Drawing.Color Object"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net]
|
||||||
|
---
|
||||||
|
I try to avoid setting styles in my code as much as possible. However, I ran across a case using a DevExpress PageControl control where there was no possible way to set a certain border by using CSS. This is because DevExpress wisely (read sarcasm) uses “border-color: #whatever !important” in the element’s style attribute. So, there is no way to override that style except by setting it on the control in code (or your aspx page).
|
||||||
|
|
||||||
|
The particular border I needed is the ContentStyle.Border border of the ASPxPageControl and I needed to set the BorderColor property which is a System.Drawing.Color object. There isn’t any straightforward way to get a Color object from a hexadecimal color using the Color object itself. You have to use the ColorTranslator (also in the System.Drawing namespace) which gives you a FromHtml method and returns the correct Color object.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
Color hexColor = ColorTranslator.FromHtml(“#666666″);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Thanks to this guy for the quick answer:
|
||||||
|
[http://www.akxl.net/labs/articles/converting-a-hexadecimal-color-to-a-system.drawing.color-object/](http://www.akxl.net/labs/articles/converting-a-hexadecimal-color-to-a-system.drawing.color-object/)
|
||||||
13
src/posts/2010/06/29/vertical-text-with-css.md
Normal file
13
src/posts/2010/06/29/vertical-text-with-css.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Vertical text with CSS"
|
||||||
|
date: 2010-06-29
|
||||||
|
description: "Vertical text with CSS"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [css]
|
||||||
|
---
|
||||||
|
I found a nice blog post today describing how to do vertical text with CSS. Instead of reposting his information, I’ll just give the link.
|
||||||
|
|
||||||
|
[http://scottgale.com/blog/css-vertical-text/2010/03/01/](http://scottgale.com/blog/css-vertical-text/2010/03/01/)
|
||||||
|
|
||||||
|
It’s a nice technique using various proprietary methods since there is no standard way to get vertical text across the major browsers right now.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Visual Studio Post Build Event to Copy DLLs"
|
||||||
|
date: 2010-06-29
|
||||||
|
description: "Visual Studio Post Build Event to Copy DLLs"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [visual-studio]
|
||||||
|
---
|
||||||
|
While doing SharePoint development, we use a lot of pre- and post-build events in Visual Studio projects to do various things like build and deploy SharePoint solutions to our development environments, activate features automatically and other similar things to make the process of building solutions for SharePoint as automated as possible.
|
||||||
|
|
||||||
|
One helpful way I’ve been able to use post-build events is to copy the compiled DLLs for a given project to the inetpub directory for the SharePoint site I am currently working on. As I’m building a new feature or debugging an existing one, I like to build a lot of see my changes as I go. Well, building and deploying the entire SharePoint solution is a rather lengthy process it’s only really necessary if you’re changing ASPX pages or updating a feature XML file or something like that. If all I did was come C# code changes, that’s too much to wait through to see my changes. So, what I do is put a small post-build event in my projects that I’m working with that copies the DLLs for that project to my SharePoint site’s inetpub directory and I skip the whole SharePoint solution build and deployment step.
|
||||||
|
|
||||||
|
Adding a post-build event couldn’t be much simpler either. In Visual Studio just right-click on the project you want to add the post-build event for in Solution Explorer and select “Properties”. You can either do that or, with the project selected in Solution Explorer, hit Alt-Enter. The project properties window will display. On the left-hand side, click the “Build Events” tab. You’ll see two boxes there. One is for pre-build events and one is for post-build events.
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
In the post-build events textbox add the following:
|
||||||
|
IF NOT ($(ConfigurationName)) == (Debug) GOTO END
|
||||||
|
cd $(ProjectDir)
|
||||||
|
copy /y bin\debug\*.dll C:\inetpub\wwwroot\wss\VirtualDirectories\{YOUR SHAREPOINT SITE DIRECTORY}\bin
|
||||||
|
copy /y bin\debug\*.pdb C:\inetpub\wwwroot\wss\VirtualDirectories\{YOUR SHAREPOINT SITE DIRECTORY}\bin
|
||||||
|
:END
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
These are just basic Windows batch commands like you’d find in any Windows batch file. This is a very basic setup, but it should be a good starting point for you to work from. All it does is check whether you are building with your Debug build configuration, if you are, then it moves to your project directory and copies the DLLs in your bin\debug folder to the bin folder of your SharePoint site.
|
||||||
|
|
||||||
|
Easy enough!
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Determine SharePoint MOSS 2007 Service Pack Version"
|
||||||
|
date: 2010-10-06
|
||||||
|
description: "Determine SharePoint MOSS 2007 Service Pack Version"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
Determining the service pack that you have installed for a particular
|
||||||
|
SharePoint 2007 installation turned out to be a surprisingly opaque process.
|
||||||
|
I found this very clear and helpful blog post dealing with just this
|
||||||
|
determination:
|
||||||
|
[http://techpunch.wordpress.com/2008/10/15/sharepoint-2007-moss-how-to-determine-service-pack-version/](http://techpunch.wordpress.com/2008/10/15/sharepoint-2007-moss-how-to-determine-service-pack-version/)
|
||||||
|
|
||||||
|
To give a quick overview, the right way to determine the installed service pack is to look up the version of SharePoint that you have installed and match it against a table (available at the link above) of version numbers to service packs.
|
||||||
|
|
||||||
|
Determine your SharePoint version by logging into your Central Administration site and going to Operations > Servers In Farm > Database Schema Version. Your SharePoint version will be listed in the table of servers in the farm.
|
||||||
|
|
||||||
|
Note that the link above only has version numbers listed through SP1 (the post is from October 2008). Use this link to get a more up-to-date listing of version numbers:
|
||||||
|
[http://www.sharepointdesignerstepbystep.com/Blog/Articles/How%20To%20find%20the%20SharePoint%20version.aspx](http://www.sharepointdesignerstepbystep.com/Blog/Articles/How%20To%20find%20the%20SharePoint%20version.aspx)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint 2007 to 2010 in-place upgrade serviceHostingEnvironment error"
|
||||||
|
date: 2010-10-06
|
||||||
|
description: "SharePoint 2007 to 2010 in-place upgrade serviceHostingEnvironment error"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
I’m testing an in-place SharePoint 2007 to SharePoint 2010 upgrade today and I came across this odd error during the update process.
|
||||||
|
|
||||||
|
I successfully installed SharePoint 2010 and proceeded to run the configuration wizard which updates 2007 to 2010. The configuration process would always fail with an error in the Central Administration web.config file. Viewing the configuration log file (the configuration wizard provided the location of it) turned up the error.
|
||||||
|
|
||||||
|
Apparently, at some point in the process the configuration wizard looks for the serviceHostingEnvironment element under the `<system.serviceModel>` section in the Central Administration web.config.
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<serviceHostingEnvironment aspNetCompatibilityEnabled=”true” />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
If it doesn’t find it adds it itself, but curiously, it wraps it in an incomplete system.serviceModel tag so it looks like this:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<system.serviceModel>
|
||||||
|
<serviceHostingEnvironment aspNetCompatibilityEnabled=”true” />
|
||||||
|
</system.serviceModel>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This then caused the configuration wizard to bomb out.
|
||||||
|
|
||||||
|
The simple solution was to just add the `<serviceHostingEnvironment aspNetCompatibilityEnabled=”true” />` element under my existing `<system.serviceModel>` section. The wizard completed just fine after this.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Error occurred in deployment step ‘Recycle IIS Application Pool’: The local SharePoint server is not available. Check that the server is running and connected to the SharePoint farm."
|
||||||
|
date: 2010-10-08
|
||||||
|
description: "Error occurred in deployment step ‘Recycle IIS Application Pool’: The local SharePoint server is not available. Check that the server is running and connected to the SharePoint farm."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
I got the above error today while trying to deploy a SharePoint 2010 project in Visual Studio 2010. Very frustrating!!
|
||||||
|
|
||||||
|
The answer turned out to be simple enough, though no clues are really provided in the error message (typical). I had to add my user account to the db_owner role for the SharePoint_Admin database that the site I was deploying to was using.
|
||||||
|
|
||||||
|
Once I did that, the deployment in Visual Studio did just great.
|
||||||
|
|
||||||
|
The following post led me to the solution: [http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/a97a8413-a0ae-47f2-aab7-fae1cf40595a](http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/a97a8413-a0ae-47f2-aab7-fae1cf40595a)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Cookie.Expires from ASP.NET Request Object Always DateTime.MinValue"
|
||||||
|
date: 2010-10-12
|
||||||
|
description: "Cookie.Expires from ASP.NET Request Object Always DateTime.MinValue"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,asp.net]
|
||||||
|
---
|
||||||
|
I’ve been working on a problem today involving cookies with one of our web applications. The cookie is keeping track of a user’s authenticated session and it’s timing people out too soon. So, I was checking the expiration time of the cookie coming in from the browser and it was returning 1/1/0001 12:00AM every time. I thought this might be the source of my problem, but resetting the cookie and the expiration time did not help the next round-trip to the server. The cookie still read the DateTime.MinValue time.
|
||||||
|
|
||||||
|
I found this article: [http://www.eggheadcafe.com/tutorials/aspnet/198ce250-59da-4388-89e5-fce33d725aa7/aspnet-cookies-faq.aspx](http://www.eggheadcafe.com/tutorials/aspnet/198ce250-59da-4388-89e5-fce33d725aa7/aspnet-cookies-faq.aspx)
|
||||||
|
|
||||||
|
The articles explains why this happens. The summary: the browser maintains the cookie and handles its expiration. Therefore, it does not send this information back to the server when making a request. So, you can never read the expiration time of a cookie on the server from the Request object.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Add SharePoint 2010 Central Admin Web Applications Page Ribbon Button"
|
||||||
|
date: 2010-11-02
|
||||||
|
description: "Add SharePoint 2010 Central Admin Web Applications Page Ribbon Button"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
I finally figured out today how to add a button to the Central Admin site’s ribbon for the WebApplicationsList page and have it behave like the other ribbon buttons on this page. The problem wasn’t so much adding the button–that part was easy. The problem was getting the button to behave like the other buttons. That is, getting it to enable and disable when a web application was selected from the list.
|
||||||
|
|
||||||
|
It was pretty easy to find the methods for the new SharePoint 2010 javascript API to see if a list item is selected in a list and then make decisions based on that. The SP.UI.ListOperation.Selection methods are great for that. However, the WebApplicationList (http:///_admin/WebApplicationList.aspx) page list in the Central Administration site is not a list and so these methods don’t work.
|
||||||
|
|
||||||
|
I had to do some digging in the SharePoint 2010 javacript files to find what I was looking for. I found what I was looking for in the C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\SP.UI.Admin.debug.js file. This is the debug version of the SP.UI.Admin.js file (so it’s possible to read it) that gets included on these pages. It includes a set of methods beginning with “SP.UI.Admin.WebApplicationPageComponent” that are used for the WebApplicationsList page.
|
||||||
|
|
||||||
|
What you want is the SP.UI.Admin.WebApplicationPageComponent.get_selectedItem method. This method returns an object containing information about the selected web application on the page.
|
||||||
|
|
||||||
|
Here is my final CustomAction XML for the button that enables the button when a web application is selected from the list and then opens up a dialog window when the button is clicked, passing the web application ID to the page that opens in the dialog box.
|
||||||
13
src/posts/2010/12/01/blank-sharepoint-site-problem.md
Normal file
13
src/posts/2010/12/01/blank-sharepoint-site-problem.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Blank SharePoint Site Problem"
|
||||||
|
date: 2011-12-01
|
||||||
|
description: "Blank SharePoint Site Problem"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
A SharePoint site renders completely blank in the browser. It shows up immediately without any “processing” time and when looking at the HTML source for the page, it is completely blank–no HTML tags whatsoever.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
In my case, the web.config file for the SharePoint application had duplicate entries in the system.webServer/handlers and system.webServer/modules elements. Once I removed those, the site rendered as expected.
|
||||||
17
src/posts/2010/12/10/duplicating-spwebconfigmodifications.md
Normal file
17
src/posts/2010/12/10/duplicating-spwebconfigmodifications.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Duplicating SPWebConfigModifications"
|
||||||
|
date: 2010-12-10
|
||||||
|
description: "Duplicating SPWebConfigModifications"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
Despite everything looking OK (no duplicate SPWebConfigModifications for your web application), your SPWebConfigModifications are adding duplicate elements to your web.config file.
|
||||||
|
|
||||||
|
## Solution (at least one possible one)
|
||||||
|
I ran into this problem today and discovered that in the Name property of the SPWebConfigModification I was not wrapping an attribute value in single quotes. Since the actual Value property of the SPWebConfigModification had quotes around the attribute value, whenever the SPWebConfigModification was applied, it could not find the existing element and therefore would add a second, duplicate element.
|
||||||
|
|
||||||
|
In other words, this is wrong for the SPWebConfigModification Name: “add[@name=valueOfAttribute]“.
|
||||||
|
|
||||||
|
This is correct: “add[@name=’valueOfAttribute’]“
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Opening all SharePoint 2010 Documents in a new window"
|
||||||
|
date: 2011-04-25
|
||||||
|
description: "Opening all SharePoint 2010 Documents in a new window"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint,javascript]
|
||||||
|
---
|
||||||
|
We wanted an easy solution to open every document in a SharePoint from a document library in a new window. This way, the user stays on the page he is originally on and the document (PDF, Word doc, Excel files–whatever) will open in a blank window.
|
||||||
|
|
||||||
|
I don’t know of any way to do this via a setting in SharePoint, but a little JavaScript employing jQuery does the job just perfectly.
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
$(document).ready(
|
||||||
|
function () {
|
||||||
|
// has to be on an interval for grouped doc libraries
|
||||||
|
// where the actual links are loaded only once a group
|
||||||
|
// is expanded
|
||||||
|
setInterval(
|
||||||
|
function () {
|
||||||
|
$("a[onclick*=’return DispEx’][target!=’_blank’]")
|
||||||
|
.attr("target", "_blank")
|
||||||
|
.removeAttr("onclick");
|
||||||
|
|
||||||
|
// document type icons
|
||||||
|
$("td.ms-vb-icon>img[onclick]:not([documentUrl])")
|
||||||
|
.click(function (e) {
|
||||||
|
window.open($(this).attr("documentUrl"), "_blank");
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.each(function () {
|
||||||
|
$(this).attr(
|
||||||
|
"documentUrl",
|
||||||
|
$.trim(String($(this).attr("onclick"))
|
||||||
|
.split("=")[1]
|
||||||
|
.replace(/["'{}]/g, "")
|
||||||
|
.split(";")[0])
|
||||||
|
);
|
||||||
|
this.onclick = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This JavaScript takes into account that some Document Libraries are grouped and so not all the document links (or icon links) will appear on the page load. This is the reason for the half-second interval. At the most half a second after the group is expanded the newly loaded links will be altered to also open in a new window.
|
||||||
|
|
||||||
|
You’ll notice that there are two things happening on each interval. The first line does any Title links for the document. The second line (and following) handles the document type icons that are also links to the document. Note that if you want only one or the other link to open in a new window (for example, on the document type icon opens in a new window and the Title link opens in the same window) then all you need to do is remove the appropriate line.
|
||||||
19
src/posts/2011/08/24/file-name-sanitizer-one-liner.md
Normal file
19
src/posts/2011/08/24/file-name-sanitizer-one-liner.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "File Name Sanitizer One-Liner"
|
||||||
|
date: 2011-08-24
|
||||||
|
description: "File Name Sanitizer One-Liner"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [command-line]
|
||||||
|
---
|
||||||
|
I find myself often having to sanitize file names and paths because I’m creating files from some kind of user input that I’m unsure about. I found this handy way of doing this in one line today:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (name, c) => name.Replace(c, ‘_’));
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Note that the same goes for paths. The only thing that changes is the static method you use from Path:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
filePath = Path.GetInvalidPathChars().Aggregate(filePath , (name, c) => name.Replace(c, ‘_’));
|
||||||
|
{% endhighlight %}
|
||||||
34
src/posts/2011/11/14/ansiutf-8-encoding-problem.md
Normal file
34
src/posts/2011/11/14/ansiutf-8-encoding-problem.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "ANSI/UTF-8 Encoding Problem"
|
||||||
|
date: 2011-11-14
|
||||||
|
description: "ANSI/UTF-8 Encoding Problem"
|
||||||
|
categories: [programming]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
I ran into a problem in a project I was working on recently. The basics of the project were to take data out of a MySQL database and transform that data into an XML file schema for a
|
||||||
|
[Day CQ5 CMS](http://day.com/day/en/products.html)
|
||||||
|
[Java Content Repository (JCR)](http://www.ibm.com/developerworks/java/library/j-jcr/).
|
||||||
|
|
||||||
|
I kept running into an issue with certain text fields of user-entered data where I would get all these funky, special characters in the text. I knew it had to be some kind of encoding issue where I wasn’t outputting or getting the data with the right character encoding.
|
||||||
|
|
||||||
|
I don’t know much about character encoding, except that there is a difference between them!
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
After some digging around and playing with Notepad++ (where I could switch the encoding of the text), I found at least a glimmer of hope. When I had the character encoding set to ANSI in Notepad++, pasted in the troublesome text, and then switched the encoding to UTF-8, all of my problems disappeared.
|
||||||
|
|
||||||
|
So, eventually, I came up with these few lines of code which convert the encoding of my text and fixed all of my problems. Woohoo!
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
Encoding targetEncoding = Encoding.GetEncoding(1252);
|
||||||
|
byte[] utf8Bytes = targetEncoding.GetBytes(text);
|
||||||
|
byte[] ansiBytes = Encoding.Convert(Encoding.UTF8,
|
||||||
|
targetEncoding,
|
||||||
|
utf8Bytes);
|
||||||
|
return targetEncoding.GetString(ansiBytes);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
What this simple code does is first get the Encoding for the ANSI character encoding (1252 is ANSI, apparently). It then gets the bytes of the string with the ANSI encoding, converts them from the ANSI encoding to UTF-8 encoding and then gets the string with the ANSI encoding.
|
||||||
|
|
||||||
|
So, quite honestly, I’m not 100% sure why this works. The Notepad++ experiment would seem to indicate the opposite: that I had to convert from ANSI to UTF-8. But, that only compounded the problem when I wrote it that way in code. When I reversed the conversion, it worked as I was hoping.
|
||||||
21
src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md
Normal file
21
src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "jQuery Tools Overlay v1.2.5 Bug"
|
||||||
|
date: 2011-11-21
|
||||||
|
description: "jQuery Tools Overlay v1.2.5 Bug"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [jquery]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
I noticed a bug in the jQuery Tools library today, specifically in version 1.2.5 and the Overlay plugin. I should say, I’m assuming it’s a bug. This is an existing website I am working on where I am not in a position to upgrade the jQuery Tools library to the latest version (1.2.6 is out, it looks like) and I don’t know how much of the problem may be due to how existing code is possibly interfering with the library.
|
||||||
|
|
||||||
|
In any case, the bug showed up when I had an existing overlay showing and wanted to show another overlay. By default, the library allows one overlay at a time. This was fine with me as I needed to close the existing overlay before showing the new one. The library handles this automatically. However, when the second overlay was displayed, the mask would not display and the z-index was not set, so it showed up behind other content.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
While it’s somewhat of a hack, the best solution I found was to simply remove the div that Overlay creates with id “exposeMask” before I display the second overlay.
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
$(“#exposeMask”).remove();
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I believe this is what is happening: when the first overlay is closed by the library, it doesn’t properly handle the existing exposeMask div and so when the second overlay shows it misunderstands the mask to be displaying properly and doesn’t calculate its own display values properly. Therefore, its z-index is wrong (or non-existing) and the mask remains hidden (because the library hid it when it closed the first overlay).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Formatting multiple XML files at a time"
|
||||||
|
date: 2011-12-14
|
||||||
|
description: "Formatting multiple XML files at a time"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [xml]
|
||||||
|
---
|
||||||
|
I recently had a situation where I needed to compare many XML files generated by a program of one version to the same set of XML files produced by a previous version of the same program. Unfortunately, the sets of XML files were formatted differently and so doing a file comparison with Beyond Compare (a GREAT file comparison tool, by the way) was going to be useless.
|
||||||
|
|
||||||
|
So, I started looking for a way to quickly format all the files in each set the same way with one program. I looked into using Notepad++ which has a great XML Tools plugin (look for it under Plugins > Plugin Manager). I tried combining the plugin’s formatting commands with a macro that would format the XML file, save it and close it. So, I could easily open the few hundred files I had to format in Notepad++ (one set at a time), then run the macro multiple times (Macro > Run a Macro Multiple Times…). This would run through each file until all were formatted and closed. However, after working with it for a while, I couldn’t get the Notepad++ macro system to actual perform the XML Tools plugin format command. The macro would successfully run, saving and closing the file. But, when I checked the files they had not been formatted. I worked with it for a while, but could not figure out what the matter was.
|
||||||
|
|
||||||
|
I knew the real solution had to be some type of command-line utility and a batch file. So, I started looking into that. The solution I ended up with was just that.
|
||||||
|
|
||||||
|
First of all, I found HTML Tidy which I could run from the Windows command line to format a file. Using a configuration file for the tidy.exe (placed in the same directory as tidy.exe and named tidcfg.ini–although neither matters, see below) that looked like this:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
indent:yes
|
||||||
|
indent-attributes:yes
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I got the formatting I wanted.
|
||||||
|
|
||||||
|
Now, all I had to do was brush up on my Windows batch command skills to run tidy.exe on multiple files. Easy enough! This is what my batch file looked like:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
for /d %%X in (C:\<path_to_parent_directory>\*) do (c:\<path_to_tidy.exe>\tidy.exe -m -xml -config c:\<path_to_tidy.ini_file>\tidycfg.ini %%X\<xml_file_name>.xml)
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I had a folder structure where there were hundreds of directories inside this one parent directory. Each of the child directories had a single XML file in it. Therefore, I needed the C:\<path_to_parent_directory>\* wildcard.
|
||||||
|
|
||||||
|
So, what this batch file does is simply look at each child directory (with the /d switch) in my parent directory. In each directory it runs (do) the tidy.exe program, tells it to modify the input file itself (-m) instead of saving the formatted XML to another file, tells it that the input file is valid XML (-xml) and then tells it where the tidycfg.ini file is (-config). Finally, it tells tidy.exe to take the current directory (%%X) and use the <xml_file_name>.xml file as the input file to format.
|
||||||
|
|
||||||
|
This little set up worked very well and quickly formatted all of my files in the same way so that I could successfully compare them with Beyond Compare.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "How to Create a Brightcove Player Logo Overlay"
|
||||||
|
date: 2012-02-03
|
||||||
|
description: "How to Create a Brightcove Player Logo Overlay"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [brightcove]
|
||||||
|
---
|
||||||
|
As simple as it ended up being, it took me quite a while to figure out how to correctly add a simple overlaying graphic to a Brightcove video player yesterday. It involves a couple of steps which I did not find clearly laid out anywhere I looked. So, I thought a nice little step-by-step blog post would be helpful to anyone else looking for the same thing.
|
||||||
|
|
||||||
|
First, Brightcove has a nice and easy method (I say that because it looks easy…I didn’t try this method) of adding logo overlays to an individual video. You just:
|
||||||
|
|
||||||
|
1. Log into your video cloud Brightcove Studio
|
||||||
|
1. Click on the Media icon in the top navigation bar
|
||||||
|
1. Find and select your video (by clicking on it)
|
||||||
|
1. Click on the Edit button at the bottom of the list of videos
|
||||||
|
1. Follow the process outlined on the Logo Overlay tab of the Edit popup
|
||||||
|
|
||||||
|
This isn’t what I wanted though. I needed these videos to appear logo-free in certain instances. So, the best solution was to use one player in the instance where I didn’t need a logo and then use a different player that included the logo overlay for my other scenario. That required add an overlay to a player, not a video.
|
||||||
|
|
||||||
|
So, on to the solution!
|
||||||
|
|
||||||
|
## Step One: Create a new player template
|
||||||
|
This part is easy. Actually, all the parts are easy. You just need to know what to do.
|
||||||
|
|
||||||
|
1. Under the Publishing tab in your Brightcove Studio, click on All Templates in the left sidebar.
|
||||||
|
1. Select one of the templates that Brightcove gives you and duplicate it (use the Duplicate button at the bottom of the list of templates)
|
||||||
|
1. Select your new template and click on the Edit button
|
||||||
|
1. In the Layout field (the XML that describes the player), add an Image element inside the Layout element. See the BEML reference documentationfor details. Here is what my Image element looks like:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<Image id="logoOverlay"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
scaleMode="exactFit"
|
||||||
|
visible="{!videoPlayer.menu.open}"/>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
All of the attributes are optional except the id attribute. It doesn’t have to be named “logoOverlay”, but you have to have something there. Notice the value for my “visible” attribute. This ensures that my logo disappears when the video is done and doesn’t awkwardly overlay any of the video menu UI elements. Remove it if you want your logo to display the whole time. Also note the “x” and “y” values. Change these to your liking. You might have to wait to finish Step Two below when you’ve added a logo and then you can test a video with your player to see where the logo fits best. From there, you can keep adjusting your logo until you’re satisfied.
|
||||||
|
1. Now click on All Players in the left sidebar and then click on the New Player button at the bottom
|
||||||
|
1. Create a new player with whatever name is appropriate from the template you just created
|
||||||
|
|
||||||
|
That’s it for the template! You’re done with Step One.
|
||||||
|
|
||||||
|
## Step Two: Add Your Logo
|
||||||
|
I figured out step one all on my own pretty quickly. I figured out step two all on my own too, but it look a lot longer!
|
||||||
|
|
||||||
|
1. Under the Publishing tab (you’re probably already there), click on All Players in the left sidebar
|
||||||
|
1. Select the player you just created in Step One and click on the Styles button at the bottom of the page.
|
||||||
|
1. If you gave your Image element an id (you should have!), then you should see an item underneath Editable Areas in the right sidebar with the same name as your Image id. Click on it.
|
||||||
|
1. Just below (also in the right sidebar), you should now see an Image Selection tab in the Edit pane
|
||||||
|
1. Click on Upload and upload your logo (or whatever image).
|
||||||
|
1. Add a URL that the image links to and a tooltip to show when you hover over the image, if you want
|
||||||
|
1. Click on Save & Close
|
||||||
|
|
||||||
|
You’re done! Now, any video you display with that player will have a logo overlay.
|
||||||
21
src/posts/2012/02/03/install-sharepoint-on-windows-7.md
Normal file
21
src/posts/2012/02/03/install-sharepoint-on-windows-7.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Install SharePoint on Windows 7"
|
||||||
|
date: 2012-02-03
|
||||||
|
description: "Install SharePoint on Windows 7"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
If you try to do a SharePoint 2010 installation on your client machine (that is, not a Windows Server OS) then you’ll get an error explaining that SharePoint 2010 can’t be installed on your operating system.
|
||||||
|
|
||||||
|
Never fear, CodeProject is here. Check out this helpful post to get it installed (it couldn’t be easier):
|
||||||
|
|
||||||
|
[CodeProject article](http://www.codeproject.com/Articles/44210/Installing-SharePoint-Server-2010-on-Windows-7-x64)
|
||||||
|
|
||||||
|
Even though the post is from 2009, it still works great.
|
||||||
|
|
||||||
|
For more detailed steps (including the ones outlined in the link above) following these instructions:
|
||||||
|
|
||||||
|
[Microsoft’s detailed steps](http://msdn.microsoft.com/en-us/library/ie/ee554869.aspx)
|
||||||
|
|
||||||
|
They will help you get the prerequisites installed as well.
|
||||||
19
src/posts/2012/02/27/ektron-license-key-update.md
Normal file
19
src/posts/2012/02/27/ektron-license-key-update.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Ektron License Key Update"
|
||||||
|
date: 2012-02-27
|
||||||
|
description: "Ektron License Key Update"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ektron]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
We use a temporary license key with our Ektron development website here at work. Because it’s temporary, we have to update it every-so-often. That happened today.
|
||||||
|
|
||||||
|
I got the new key and copy-pasted it into the right License Key field under Configuration / Settings. I clicked on “Save” and thought I was done. When I tried to log back in with my regular account (the way you are notified of the license expiration is a license violation error when you try log in) and it still gave me the license violation error. So, I logged back in with the built-in account (which lets you log in with limited features so that you can update the license key) and found out that I had copy-pasted the license key wrong. It was missing part of the domain name.
|
||||||
|
|
||||||
|
Naturally, I tried simply updating the license key domain name, but Ektron threw a Javascript error which prevented me from saving the page saying that I had updated the license key in a way that would make it invalid. What?! I tried re-pasting the key in, thinking I had it wrong still. I didn’t work.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Turns out this has to be an Ektron bug. I ended up trying something random: I changed the last number of the license key to something arbitrary (along with making my domain name change) and it updated fine. Strange, I know…but this IS Ektron.
|
||||||
|
|
||||||
|
Of course, that is an invalid key, so once the page saved, I changed that last digit back to the original digit from the valid license key and it let me save it and I could log in again.
|
||||||
168
src/posts/2012/03/05/grouping-into-rows-with-xslt.md
Normal file
168
src/posts/2012/03/05/grouping-into-rows-with-xslt.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Grouping into Rows with XSLT"
|
||||||
|
date: 2012-03-05
|
||||||
|
description: "Grouping into Rows with XSLT"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [xslt]
|
||||||
|
---
|
||||||
|
Let’s say you have a simple XML document that looks something like this:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<root>
|
||||||
|
<Pod id="1"></Pod>
|
||||||
|
<Pod id="2"></Pod>
|
||||||
|
<Pod id="3"></Pod>
|
||||||
|
<Pod id="4"></Pod>
|
||||||
|
</root>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
The content or meaning of the pods is irrelevant. The idea is that you have a “list” of elements (pods in this case) in your XML. How, then, do you use XSLT to group these pods into rows of two, three or whatever number of pods across? Something like this:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="pod"> pod 1 </div>
|
||||||
|
<div class="pod"> pod 2 </div>
|
||||||
|
<div class="pod"> pod 3 </div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="pod"> pod 4 </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This example would required three pods per row. The example XSLT below will work for any number of pods per row with a simple edit (explained below).
|
||||||
|
|
||||||
|
My first thought was from a very normal programmer’s perspective. I’d use some kind of looping (with xsl:for-each) and just start a new row for each x number of pods. So, I did it that way and it worked. It was pretty ugly XSLT, but I didn’t really think there was a better way to do it. But then a colleague looked over my shoulder and chuckled. “I did it that way at first too”, he said, “but that’s not the XSLT way to do it.” He gave me a little bit of a clue of how it really should be done then he left, leaving me determined to find the “XSLT way” of doing it!
|
||||||
|
|
||||||
|
Turns out, it really isn’t difficult. You just have to think a little differently and start using xsl:template, xsl:call-template and xsl:apply-templates effectively along with their select and match attributes. So, take a look at this XSLT file:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xsl:stylesheet version="1.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
|
||||||
|
exclude-result-prefixes="msxsl">
|
||||||
|
<xsl:output method="html"
|
||||||
|
version="1.0"
|
||||||
|
omit-xml-declaration="yes"
|
||||||
|
indent="yes"
|
||||||
|
encoding="utf-8" />
|
||||||
|
|
||||||
|
<xsl:template match="/root">
|
||||||
|
<xsl:apply-templates select="Pod[position() mod 3 = 1]" />
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodRow"
|
||||||
|
match="Pod[position() mod 3 = 1]">
|
||||||
|
<div class="row">
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:call-template name="PodWrapper"/>
|
||||||
|
<xsl:apply-templates
|
||||||
|
select="following-sibling::Pod[position() < 3]"/>
|
||||||
|
</xsl:choose>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodWrapper"
|
||||||
|
match="Pod[position() mod 3 > 1]">
|
||||||
|
<div class="pod">
|
||||||
|
<xsl:call-template name="PodContent"/>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodContent">
|
||||||
|
<!– whatever content your "pods" contain can be marked up here –>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Here’s what’s going on: first, we call xsl:apply-templates on the first and then every third Pod using “Pod[position() mod 3 = 1]”. This will select the Pod at the start of every row: Pods 1 and 4. Notice that XSLT indexing starts at 1 and NOT 0. The PodRow template will match these selected Pods.
|
||||||
|
|
||||||
|
Inside the PodRow template we have the HTML for the row. Inside the row HTML, we explicitly call the PodWrapper template for the current Pod. We then call xsl:apply-templates on the following siblings of the current Pod, but only on the next two of them. This grabs the remaining Pods in the row. This xsl:apply-templates select will match the PodWrapper template which creates the HTML for a single Pod.
|
||||||
|
|
||||||
|
Easy as pie! To change the number of pods in a row, update all of the “position() mod 3″s by replacing 3 with however many pods you want in a row. Then also make sure to update the xsl:apply-templates select attribute inside PodRow to be “position() < n-1″ where n is the number of Pods in a row.
|
||||||
|
|
||||||
|
## Identifying the last cell of every row
|
||||||
|
I needed another feature from my XSLT though. I needed every last pod in each row to have a special CSS class. This meant determining which pod was the last one in each row whether the row was full or not. This made the XSLT significantly more complicated, but not overwhelmingly so.
|
||||||
|
|
||||||
|
I needed my HTML to look like this (notice the additional “last” class):
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="pod"> pod 1 </div>
|
||||||
|
<div class="pod"> pod 2 </div>
|
||||||
|
<div class="pod last"> pod 3 </div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="pod last"> pod 4 </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
So, this is what I had to do:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xsl:stylesheet version="1.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
|
||||||
|
exclude-result-prefixes="msxsl">
|
||||||
|
<xsl:output method="html"
|
||||||
|
version="1.0"
|
||||||
|
omit-xml-declaration="yes"
|
||||||
|
indent="yes"
|
||||||
|
encoding="utf-8" />
|
||||||
|
|
||||||
|
<xsl:template match="/root">
|
||||||
|
<xsl:apply-templates select="Pod[position() mod 3 = 1]" />
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodRow"
|
||||||
|
match="Pod[position() mod 3 = 1]"
|
||||||
|
priority="2">
|
||||||
|
<div class="row">
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="count(following-sibling::Pod) = 0">
|
||||||
|
<xsl:call-template name="PodWrapperLast"/>
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:otherwise>
|
||||||
|
<xsl:call-template name="PodWrapper"/>
|
||||||
|
<xsl:apply-templates
|
||||||
|
select="following-sibling::Pod[position() < 3]"/>
|
||||||
|
</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodWrapper"
|
||||||
|
match="Pod[position() mod 3 > 1]"
|
||||||
|
priority="0">
|
||||||
|
<div class="pod">
|
||||||
|
<xsl:call-template name="PodContent"/>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodWrapperLast"
|
||||||
|
priority="1"
|
||||||
|
match="Pod[position() mod 3 = 0 or position() = last()]">
|
||||||
|
<div class="pod last">
|
||||||
|
<xsl:call-template name="PodContent"/>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="PodContent">
|
||||||
|
<!– whatever content your "pods" contain can be marked up here –>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This version of the XSLT functions exactly the same as the previous one. The only additions are for identifying the last pod in each row for all cases. So, here is what has changed.
|
||||||
|
|
||||||
|
The xsl:apply-templates call inside of PodRow now matches one of two templates: PodWrapper or PodWrapperLast. You guessed it, PodWrapperLast just has an extra “last” class on the Pod div and a fancy match attribute. “Pod[position() mod 3 = 0 or position() = last()]” will match Pod numbers 3, 6, etc (in other words, the last Pod of every row) or the last Pod in the XML (no matter what number it is).
|
||||||
|
|
||||||
|
Notice the new xsl:choose inside of our PodRow template. This handles the case where the last Pod in the XML is also the first Pod in a row.
|
||||||
|
|
||||||
|
Notice also the “priority” attributes on the templates now. This prevents the PodWrapperLast template from matching the first Pod in a row if that first Pod is also the last Pod in the XML.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Easy Way to Make Huge Maps with Google Maps"
|
||||||
|
date: 2012-03-07
|
||||||
|
description: "Easy Way to Make Huge Maps with Google Maps"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [google-maps]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Easy way to make huge maps with Google Maps](http://www.metacafe.com/watch/1073912/google_maps_hack_how_to_save_large_maps/)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Change the color of an icon with Gimp"
|
||||||
|
date: 2012-03-15
|
||||||
|
description: "Change the color of an icon with Gimp"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [gimp]
|
||||||
|
---
|
||||||
|
Fairly often, I run across the perfect icon that I need for a website, but it’s just the wrong color. I want to keep the nice blending to the background of the icon around the edges, the shape and everything that makes the icon look spiffy. I just want the main color to be something else.
|
||||||
|
|
||||||
|
It took me a long time to figure out a good method (I’m no graphic designer!), but I eventually came a cross a really easy method that works well using trusty Gimp. I’ll walk you through it.
|
||||||
|
|
||||||
|
## 1. Find an Icon
|
||||||
|
|
||||||
|
The first step, of course, is to find an icon. There are billions of free ones out there on the web. This is mine.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I love it. It’s just what I need–but what a terrible color! I need something else. It’s a PNG and when I open it in Gimp, here’s what it looks like.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can see the edges have been blended into a white background which would work great if I only ever put it on a white background. Well, I just might be doing that, but I’m too much of a perfectionist to use it like that. So, let’s get to work.
|
||||||
|
|
||||||
|
## 2. Create a color layer
|
||||||
|
|
||||||
|
Add a new layer to the image by selecting Layer > New Layer. Call it “color”. Make sure it’s on top of the “Background” layer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Fill this new layer with the color you want to change the icon to. I’m using this lovely puke color (not my choice!): #afbd0a.
|
||||||
|
|
||||||
|
Now, hide this new layer so we can work on the “Background” layer by clicking on the eye button just to the left of the layer thumbnail.
|
||||||
|
|
||||||
|
## 3. Manipulate the icon colors
|
||||||
|
|
||||||
|
Select the “Background” layer. Desaturate the image by selecting Colors > Desaturate. I choose “Lightness” from the dialog box that pops up.
|
||||||
|
|
||||||
|
Now, invert the image by selecting Colors > Invert. Next, create another layer called “mask” and fill it with black. Move this layer below the “Background” layer. Your layers should look like the image below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Right-click on the “Background” layer and select “Merge down”. Now you should have something similar to the image below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 4. Adjust the curves
|
||||||
|
|
||||||
|
Now, with the “mask” layer selected, select Colors > Curves. In the dialog, drag the very middle of the line straight up to the top-center of the graph. Gimp should look like the screenshot below. Click “OK”.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 5. Create a layer mask on the “color” layer
|
||||||
|
|
||||||
|
Now, with the “mask” layer selected, copy the entire layer by selecting Select > All. Then select Edit > Copy.
|
||||||
|
|
||||||
|
Next, right-click on the “color” layer and select “Add layer mask”. Choose “Black (full transparency)” and make sure “Invert mask” is checked.
|
||||||
|
|
||||||
|
Paste what you’ve copied to the layer mask by selecting Edit > Paste. You’ll notice a new layer called “Floating selection (Pasted layer)”. Right-click on this new layer and select “Anchor layer”. The pasted layer will now be merged with your anchor layer.
|
||||||
|
|
||||||
|
## 6. You’re done!
|
||||||
|
|
||||||
|
Now hide your “mask” layer and show your “color” layer. Perfect!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You now have a new icon with a completely different color, but with the nice blending at the edges and perfect shape of the original icon. Don’t believe me? Well, test it! Select your “mask” layer and delete everything on it. Fill it with any background color you want and see how it looks. Make sure to show your “mask” layer again by click on the eye button.
|
||||||
|
|
||||||
|
Hope this was helpful! You can apply your layer mask on the “color” layer if you want. But, I like to keep it as it is and save the file as an xcf for later use. This way, if you ever need the icon in another color, all you have to do is change the fill color of the “color” layer and you’re done.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Limit Selected Items on Sitecore Multilist Field"
|
||||||
|
date: 2012-06-12
|
||||||
|
description: "Limit Selected Items on Sitecore Multilist Field"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitecore]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
You want to limit the number of items a user can choose for a multilist field in a data template in sitecore.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
The solution is easy enough. Just add some validation to the field that will “count” the number of GUIDs in the raw value of the MultiList field.
|
||||||
|
|
||||||
|
1. Open up the template in the tree navigation and click on the MultiList field you want to limit
|
||||||
|

|
||||||
|
|
||||||
|
2. Scroll down to the Validation field and enter this: ^(\{[^}]+\}\|?){0,5}$. This regular expression simply makes sure you have between 0 and 5 GUIDs in the value of the MultiList, effectively limiting the number of items you select in the list. The {0,5} is the range. So, change the 0 to limit the lower bound and change the 5 to limit the upper bound.
|
||||||
|

|
||||||
|
|
||||||
|
3. Enter an appropriate message that explains why the validation failed in the ValidationText field.
|
||||||
|

|
||||||
43
src/posts/2012/07/19/grace-and-me.md
Normal file
43
src/posts/2012/07/19/grace-and-me.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Grace & Me"
|
||||||
|
date: 2012-07-19
|
||||||
|
description: "Grace & Me"
|
||||||
|
categories: [personal]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
Our story really begins somewhere around the beginning of 2007. That’s when Grace’s brother Nathan came down to Kansas City for a couple of Bible conferences that I (Ben) also happened to go to. I barely remember meeting Nate, but he must have made an impression because we started to become good friends soon afterward. On a whim one time as Nate and I were chatting on Facebook, he invited me up to a regular men’s Bible study in Colfax, WI led by Warren Henderson. That’s eight hours away from Kansas City and so Nate never thought I’d actually say yes. But, what Nate did not know was this was a tremendous answer to prayer for me. I’d been praying that the Lord would bring some good, solid young men who were my age into my life. This seemed like the perfect place to see that prayer answered. Little did I know that it would some day lead to me marrying Nate’s sister!
|
||||||
|
|
||||||
|
Grace and I don’t really know for sure when we first met. We have our first, real evidence of being in the same place at the same time at the 2008 Vessels of Honor conference in Parkville, MO. But, it was probably a little before that, on a Sunday morning in Chippewa Falls, WI after one of those men’s Bible studies at the River Valley Christian Fellowship where Nathan, Grace and their family went to church.
|
||||||
|
|
||||||
|
It’s pretty safe to say that neither Grace nor I really took too much notice of each other back then! I was just some friend of Nathan’s, driving up ridiculous distances for a one-day Bible study, and she was just Nathan’s younger sister.
|
||||||
|
|
||||||
|
As time went on, Nathan got me involved in Story Book Lodge Bible Camp in Gilbert, MN. That’s even further north–almost as far north as you can get in Minnesota. Counseling at camp in the summer and attending Winter Retreat up at camp became a yearly event. Grace and I slowly got to know each other better as we attended the same camps and conferences over the next four years.
|
||||||
|
|
||||||
|
Still, while we gradually became friends, as late as June of 2011 we would never have thought anything beyond friendship was in our future.
|
||||||
|
|
||||||
|
As the summer of 2011 rolled along, Grace and I were both having some gracious and immensely valuable lessons from the Lord sink into our minds and hearts. Through completely separate, but similar situations we were both learning what trusting in the true goodness of our Lord meant. It was clear to both of us that we sure wanted to get married. But, who would it be to? When would it be? Would it ever happen? We had to trust the Lord that if He is good (and He is), then being single our whole lives would be the very best life we could imagine. If, on the other hand, being married some day was what the Lord had in store for us, the timing He had planned would be the only right timing there was.
|
||||||
|
|
||||||
|
Then one day in the middle of that summer, Grace came to my mind. At the same time, I was nowhere near Grace’s mind! I was planning on counseling at Story Book Lodge’s 2nd Teen camp week and hoping Grace would be there. I was hoping I could just get to know her better. I prayed a lot about it. I asked the Lord that He would make things clear if I should take the next step after this week at camp. Amazingly, others were secretly praying at this same time specifically for me and Grace too–I just didn’t know about it!
|
||||||
|
|
||||||
|
Meanwhile, Grace wasn’t even sure if she would make it to camp that year. It wasn’t clear if she could get off of work in time. But, at the last minute, she found out it was going to work and she was able to counsel that same week.
|
||||||
|
|
||||||
|
I spent the week concentrating on the spiritual needs of my campers, but also taking what opportunities presented themselves (or that I could generate!) to be around Grace and talk with her. She, by her own admission, was completely oblivious to this while I was taking each smile or glance as a sure sign that she felt the same way about me that I was beginning to feel about her.
|
||||||
|
|
||||||
|
The week was full of great happenings: exciting conversations with Grace, forceful proddings from her brother-in-law Cory, knowing looks from her sister Beth and finally suspicion on Grace’s part too. Then, the week came to a close. Camp is often a perception altering vortex of emotion. So, I wanted to be sure what I was thinking and feeling wasn’t a product of the strange air at Story Book (they don’t call it that for nothing, you know).
|
||||||
|
|
||||||
|
So, I waited a week after camp to just think and pray. Meanwhile, Grace was wondering if her own suspicions were part of the Story Book vortex. She was praying for clear thinking about it too.
|
||||||
|
|
||||||
|
The following Sunday though, I got up the courage to call her Dad and ask his permission to start pursuing Grace. I was nervous, but he gladly said yes and I called Grace a few minutes later. Thankfully, she said yes too!
|
||||||
|
|
||||||
|
What followed were some of the happiest months of our lives so far. We traveled to Eau Claire or Kansas City as much as we could, trying to spend as much time together as possible. It seemed like things just went from good to better. We agreed on just about everything we could think to talk about and slowly but surely we fell rather deeply in love with each other.
|
||||||
|
|
||||||
|
As February of 2012 rolled around, I knew I wanted to marry this girl. I bought a ring and made my plan to visit the Hanson’s over a week from the 17th to the 26th while the newly engaged David and Rachel (now) Hanson were visiting from California. A fishing trip was planned for that first Saturday which in Wisconsin means you’re getting up at 4am. But, I couldn’t wait any longer. I asked Joel (Grace’s Dad) if I could talk to him that morning before we left for fishing and he seemed to know what might be coming. So, we both got up around 3:30 and I asked him if I could marry his daughter. Yes, was the answer! Now I had a little more planning to do.
|
||||||
|
|
||||||
|
I found a fire tower not too far away where Grace and I could climb up and watch the sunrise together. I imagined a beautiful morning, no clouds and a little nice, wooden room on top of the fire tower where I could propose and we could eat a little breakfast together. What happened was slightly less perfect than I had hoped! It was a freezing cold morning with a frigid wind whipping through the broken windows of the years-unused fire tower. We even had a tired onlooker–a racoon making it’s bed in the ceiling! But, the morning really was crystal clear and the sunrise was beautiful. We read some Psalms together and I told Grace for the first time that I loved her. I asked her to marry me and she quietly said yes.
|
||||||
|
|
||||||
|
And so, the wedding planning began and here we now are, less than two months from the day! What we thought were wonderful days while we were dating paled in comparison to the joy of being engaged, looking forward every moment to the day of our wedding and beyond, all the while getting to know each other in a deeper and closer way.
|
||||||
|
|
||||||
|
Grace and I are so thankful for the goodness of the Lord. That very goodness that He so graciously and patiently taught both of us has now been poured out to us in ways we hardly know how to express. He has given us salvation through His Son, Jesus Christ. He has given us wonderful, strong families who love us and care for us. He has given us so many good, encouraging friendships. He has given us to each other. What a wonderful God we serve!
|
||||||
|
|
||||||
|
Grace and I can think of no better way to return our thanks to the Lord than to serve Him with whole hearts for the rest of our lives. Whatever the Lord has for us, may His glory be shown and His honor upheld through our lives together! That is our desire for our marriage.
|
||||||
25
src/posts/2012/07/25/nested-user-controls-not-rendering.md
Normal file
25
src/posts/2012/07/25/nested-user-controls-not-rendering.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Nested User Controls Not Rendering"
|
||||||
|
date: 2012-07-25
|
||||||
|
description: "Nested User Controls Not Rendering"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitecore]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
I got caught today in a stupid problem. But, it wasn’t obvious (at least to me) at first what was causing it. I was doing something simple: nesting one UserControl inside of another. But, the nested UserControl wasn’t showing up. I couldn’t, for the life of me, figure out what I had done wrong. If I didn’t nest the control, it worked fine.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
The solution was just correctly a stupid oversight on my part. I use ReSharper, so what I would do is just type the control prefix, colon then the name of the control and ReSharper would give me the suggestion to add the right Register tag. Well, ReSharper looks at it like a WebControl, not a UserControl. So, the Register tag is adds looks like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
<%@ Register TagPrefix="ogden" Namespace="Ogden.Web.controls" Assembly="Ogden.Web" %>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
It should look like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
<%@ Register TagPrefix="ogden" TagName="BlogComments" src="BlogComments.ascx" %>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
So, if you’re dumb like me, make sure you have the right Register tag if you’re nesting a UserControl (or using a UserControl anywhere really).
|
||||||
22
src/posts/2012/07/27/group-list-into-sub-lists-by-index.md
Normal file
22
src/posts/2012/07/27/group-list-into-sub-lists-by-index.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Group List Into Sub-lists by Index"
|
||||||
|
date: 2012-07-27
|
||||||
|
description: "Group List Into Sub-lists by Index"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
Everyone once-in-a-while I need to take a flat list (or array, or whatever) of items and divide it up into chunks. It doesn’t have to be in any real order, necessarily, it just has to be in groups of two, three or four or however many items. Most recently, I needed this to divide up a list of blog items into chunks of four to display in sets of “pages” that a user would page through with Javascript.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
I found a great, compact solution on Stack Overflow here: [http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq](http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq). Take a look at answer number one and make sure to pay attention to the warnings others gave about performance for large sets of items. I’m working on a small set, so it’s not too important in my case.
|
||||||
|
|
||||||
|
Here’s the code I used to set a Repeater DataSource:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
PodsRepeater.DataSource = Data.Items
|
||||||
|
.Select((x, i) => new { Index = i, Value = x })
|
||||||
|
.GroupBy(obj => obj.Index / 4)
|
||||||
|
.Select(obj => obj.Select(v => v.Value).ToList());
|
||||||
|
{% endhighlight %}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Rendering must have placeholder chrome as its parent. Got ‘rendering’ instead"
|
||||||
|
date: 2012-08-09
|
||||||
|
description: "Rendering must have placeholder chrome as its parent. Got ‘rendering’ instead"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitecore]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
I got the error message (in my Firebug console) in the Sitecore Page Editor on a page where I had a nested sublayout. I had a basic two-column layout for the main sublayout of the page. Then, in the right column, I had another sublayout that further divided up the right column as I needed. Here is what the nested sublayout looked like:
|
||||||
|
|
||||||
|
{% highlight html %}
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnTop"/>
|
||||||
|
|
||||||
|
<div class="sidebar-blog">
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnBottomLeft"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-ads">
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnBottomRight"/>
|
||||||
|
</div>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This was a Javascript error that was preventing the Page Editor from fully loading the editor buttons for the edit frames I had on the page.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
The error message (surprise, surprise) was a little mystifying, but it gave me enough of a clue to think that there was some type of nesting issue. I noticed that I had a sc:Placeholder element at the top of my nested sub-layout with nothing wrapping it. Perhaps this was the issue? I tried this edit to my nested sub-layout:
|
||||||
|
|
||||||
|
{% highlight html %}
|
||||||
|
<div>
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnTop"/>
|
||||||
|
|
||||||
|
<div class="sidebar-blog">
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnBottomLeft"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-ads">
|
||||||
|
<sc:Placeholder runat="server" Key="blogRightColumnBottomRight"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Notice the single, wrapping div. This worked! So, I’m not sure why, but apparently it solves this nesting problem. Perhaps Sitecore just does not expect a sc:Placeholder to be directly inside of another sc:Placeholder and so wrapping it solves that direct nesting issue.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "'Specified method is not supported' error when deleting SharePoint site"
|
||||||
|
date: 2012-09-25
|
||||||
|
description: "'Specified method is not supported' error when deleting SharePoint site"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
I was getting a “Specified method is not supported” error when I tried to delete some test sites in SharePoint 2010 today. I found a great link that gave several solutions to deletion errors. Issue #1 under this link is what I needed:
|
||||||
|
|
||||||
|
[SharePoint 2010: Unable to delete site/web after SP1](http://www.benramey.com/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site/#)
|
||||||
|
|
||||||
|
Even though I got an error executing the Upgrade-SPContentDatabase PowerShell command, it still fixed the problem with deleting the sites.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Deploying SharePoint 2010 Page Layouts with Visual Studio 2010"
|
||||||
|
date: 2012-09-26
|
||||||
|
description: "Deploying SharePoint 2010 Page Layouts with Visual Studio 2010"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[Deploying SharePoint 2010 Page Layouts with Visual Studio 2010](http://www.benramey.com/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010/#)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls"
|
||||||
|
date: 2012-09-26
|
||||||
|
description: "SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls](http://www.benramey.com/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls/#)
|
||||||
78
src/posts/2012/09/27/cannot-remove-file-error-code-158.md
Normal file
78
src/posts/2012/09/27/cannot-remove-file-error-code-158.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Cannot remove file “master_page_name_here”. Error Code: 158"
|
||||||
|
date: 2012-09-27
|
||||||
|
description: "Cannot remove file “master_page_name_here”. Error Code: 158"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
UPDATE (9/19/2013):
|
||||||
|
Please see Jeff’s comment below for a better solution to this issue using best-practices for iterating through a SPSite’s AllWebs collection.
|
||||||
|
There are multiple places online that provide an answer to the error in the title of this post. The scenario is pretty simple: deleting a master page file previously deployed by a feature in the FeatureDeactivating method of a FeatureReceiver.
|
||||||
|
|
||||||
|
Posts like this one [http://sharepoint.stackexchange.com/questions/41358/delete-custom-master-page-error-on-feature-deactivation](http://sharepoint.stackexchange.com/questions/41358/delete-custom-master-page-error-on-feature-deactivation) give the right answer (even if it’s sloppy–you don’t need to loop through EACH SPWeb and try to delete the files) but you can still get the error if you’re not careful (like I wasn’t!).
|
||||||
|
|
||||||
|
Here was my specific case. In my FeatureDeactivating method I was setting all the SPWebs back to the original v4.master like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
foreach (SPWeb site in siteColl.AllWebs)
|
||||||
|
{
|
||||||
|
site.MasterUrl = masterUrl;
|
||||||
|
site.CustomMasterUrl = masterUrl;
|
||||||
|
site.Update();
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Notice how I am looping through the SPSite.AllWebs SPWebCollection and updating each SPWeb after I reset the master page URLs. Now, here is what I was doing to try to delete the master page file from the _catalogs/masterpage library:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
string fileUrl = SPUrlUtility.CombineUrl(
|
||||||
|
siteColl.ServerRelativeUrl,
|
||||||
|
file.FullRelativeUrl);
|
||||||
|
SPFile spFile = siteColl.RootWeb.GetFile(fileUrl);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (spFile.Exists)
|
||||||
|
{
|
||||||
|
spFile.Delete();
|
||||||
|
spFile.Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
When I would get to the spFile.Delete() line, it would catch the exception whose error message is the title of this post. But why?? The master page wasn’t being referenced anywhere anymore! As I would discover, the problem is on this line:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
SPFile spFile = siteColl.RootWeb.GetFile(fileUrl);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Do you see it? I didn’t at first either. The RootWeb SPWeb reference isn’t pointing to the same object that the same SPWeb in the SPSite.AllWebs collection is pointing to. So, technically, the RootWeb object still thinks its master page URLs are pointing to my custom master that I’m trying to delete because it hasn’t had Update() called on it.
|
||||||
|
|
||||||
|
To fix it, I had to get my SPFile from one of the SPWebs in SPSite.AllWebs, like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
SPFile spFile = siteColl.AllWebs.First().GetFile(fileUrl);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
That enables me to delete the file with no errors.
|
||||||
|
|
||||||
|
## Jeff's comment
|
||||||
|
Hi Ben,
|
||||||
|
Great post but I did run into the same issue you were experiencing today. You can fix this issue also by using recommended best practice ([http://msdn.microsoft.com/en-us/library/aa973248(v=office.12).aspx](http://msdn.microsoft.com/en-us/library/aa973248(v=office.12).aspx)) –
|
||||||
|
|
||||||
|
Good Coding Practice #2
|
||||||
|
|
||||||
|
When iterating through SPWebs dispose of items with the using statement. This will cleanup any issues left in memory
|
||||||
|
|
||||||
|
So it might look like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
using(SPSite siteColl = properties.Feature.Parent as SPSite)
|
||||||
|
{
|
||||||
|
foreach (SPWeb site in siteColl.AllWebs)
|
||||||
|
{
|
||||||
|
site.MasterUrl = masterUrl; site.CustomMasterUrl = masterUrl; site.Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Endless Authentication Prompts for SharePoint Site Collection"
|
||||||
|
date: 2012-09-27
|
||||||
|
description: "Endless Authentication Prompts for SharePoint Site Collection"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[Fix for endless authentication prompts in SharePoint on localhost with host header](http://www.benramey.com/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection/#)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Check for a Publishing SPWeb or SPSite"
|
||||||
|
date: 2012-09-28
|
||||||
|
description: "Check for a Publishing SPWeb or SPSite"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[Check for a Publishing SPWeb or SPSite](http://www.dhirendrayadav.com/2011/09/checking-if-site-is-publishing-site.html)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Retrieve Dynamic Module Choice Field Value Options in Sitefinity"
|
||||||
|
date: 2012-10-05
|
||||||
|
description: "Retrieve Dynamic Module Choice Field Value Options in Sitefinity"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
I am testing out Sitefinity right now for an upcoming project. So far, I really like everything I see. It’s a great CMS and should fit out needs just perfectly (for pretty cheap too).
|
||||||
|
|
||||||
|
As with all software though, you will inevitably run into things you can’t do so easily (or, at least, as easily as you think you should be able to). I ran into one of those issues yesterday and finally hacked my way to a solution this morning.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
My problem was this: how do I retrieve the available options for a Choices field in a dynamic module (built with the Module Builder). Honestly, I’m not sure it really matters. That is, I don’t think Sitefinity validates the value of a Choices field against what is technically a “valid” option. I think I read somewhere that it can store any option. However, I wanted to present a drop-down to users that had values that were essentially controlled by the available choices for this field. Here’s the dialog I’m talking about:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Finding a way to retrieve this options through the Sitefinity API (which, in all other respects I’ve investigated so far is really good) was impossible. I started decompiling Sitefinity DLLs to see how it was done and still didn’t really get anywhere. All I could see was the choices being picked up from a config file.
|
||||||
|
|
||||||
|
As I read more, I realized this was a bigger clue than I initially thought. The dynamic module information is stored in the App_Data/Sitefinity/Configuration directory in the DynamicModulesConfig.config file. Lo and behold, the options were stored in there–in no less than three places too. Sitefinity has the Config.Get interface for it’s configuration files. However, to get the DynamicModulesConfig.config information, I would have had to call something like Config.Get(). But, the DynamicModulesConfig (in Telerik.Sitefinity.DynamicModules.Configuration) is an internal class. So, I couldn’t do that. I decided upon a “get config file and parse the xml” approach.
|
||||||
|
|
||||||
|
Here is my eventual solution. Unless they change the file location or configuration XML structure, it’s pretty safe, albeit a little hacky. I created a simple DynamicModulesHelper class.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public static class DynamicModuleHelper
|
||||||
|
{
|
||||||
|
public const string DynamicModulesConfigRelativePath =
|
||||||
|
"~/App_Data/Sitefinity/Configuration/DynamicModulesConfig.config";
|
||||||
|
|
||||||
|
public static KeyValuePair<string, string>[] GetChoiceFieldOptions(
|
||||||
|
string contentType,
|
||||||
|
string dataFieldName)
|
||||||
|
{
|
||||||
|
XmlNodeList choicesNodes = GetChoiceNodes(contentType, dataFieldName);
|
||||||
|
if (choicesNodes == null)
|
||||||
|
{
|
||||||
|
return new KeyValuePair<string, string>[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var choices = new List<KeyValuePair<string, string>>();
|
||||||
|
foreach (XmlNode node in choicesNodes)
|
||||||
|
{
|
||||||
|
ParseChoiceElement(choices, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return choices.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseChoiceElement(
|
||||||
|
List<KeyValuePair<string, string>> choices,
|
||||||
|
XmlNode node)
|
||||||
|
{
|
||||||
|
if (node == null || node.Attributes == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key, value;
|
||||||
|
key = value = string.Empty;
|
||||||
|
if (node.Attributes["text"] != null)
|
||||||
|
{
|
||||||
|
key = node.Attributes["text"].Value;
|
||||||
|
}
|
||||||
|
if (node.Attributes["value"] != null)
|
||||||
|
{
|
||||||
|
value = node.Attributes["value"].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
choices.Add(new KeyValuePair<string, string>(key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static XmlNodeList GetChoiceNodes(
|
||||||
|
string contentType,
|
||||||
|
string dataFieldName)
|
||||||
|
{
|
||||||
|
string dynamicModulesConfigPath = HttpContext.Current.Server.MapPath(
|
||||||
|
DynamicModulesConfigRelativePath);
|
||||||
|
XmlDocument xml = new ConfigXmlDocument();
|
||||||
|
string xpath = string.Concat(
|
||||||
|
"/dynamicModulesConfig/contentViewControls",
|
||||||
|
string.Format("/contentViewControl[@contentType='{0}’]", contentType),
|
||||||
|
"//view[@displayMode=’Read’]",
|
||||||
|
string.Format("//field[@dataFieldName='{0}’]", dataFieldName),
|
||||||
|
"/choicesConfig/element"
|
||||||
|
);
|
||||||
|
|
||||||
|
xml.Load(dynamicModulesConfigPath);
|
||||||
|
XmlNodeList choicesNodes = xml.SelectNodes(xpath);
|
||||||
|
return choicesNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
To get the Choices field options, you call the GetChoiceFieldOptions method, passing in the dynamic module’s content type string (like this one: “Telerik.Sitefinity.DynamicTypes.Model.TestModule.TestModule”) and the name of the Choices field (like this: “RandomChoices”). So, my particular call for just a goofy test module I created was this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
KeyValuePair<string, string>[] choices = DynamicModuleHelper.GetChoiceFieldOptions(
|
||||||
|
"Telerik.Sitefinity.DynamicTypes.Model.TestModule.TestModule",
|
||||||
|
"RandomChoices");
|
||||||
|
{% endhighlight %}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Sitefinity Configuration File Changes When Creating and Activating a Dynamic Module"
|
||||||
|
date: 2012-10-10
|
||||||
|
description: "Sitefinity Configuration File Changes When Creating and Activating a Dynamic Module"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
After you create and activate a dynamic module with Module Builder in Sitefinity several configuration files are changes inside of App_Data/Sitefinity/Configuration. Here’s a list of what is changed.
|
||||||
|
|
||||||
|
## ContentViewConfig.config
|
||||||
|
A config:link element is added under contentViewControls. Example:
|
||||||
|
{% highlight xml %}
|
||||||
|
<config:link definitionName="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition" path="dynamicModulesConfig/contentViewControls/Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition" module="ModuleBuilder" />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## DynamicModulesConfig.config
|
||||||
|
And entire contentViewControl section is added under the contentViewControls element. Example:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<contentViewControl contentType="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" managerType="Telerik.Sitefinity.DynamicModules.DynamicModuleManager, Telerik.Sitefinity" useWorkflow="True" definitionName="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition">
|
||||||
|
<views>
|
||||||
|
<view gridCssClass="sfPagesTreeview" searchFields="Title" doNotBindOnClientWhenPageIsLoaded="False" allowPaging="True" allowUrlQueries="True" disableSorting="False" itemsPerPage="50" canUsersSetItemsPerPage="False" sortExpression="LastModified DESC" detailsPageId="00000000-0000-0000-0000-000000000000" webServiceBaseUrl="~/Sitefinity/Services/DynamicModules/Data.svc/" templateEvaluationMode="None" itemsParentId="00000000-0000-0000-0000-000000000000" renderLinksInMasterView="True" enableSocialSharing="False" displayMode="Read" useWorkflow="True" title="Config changes" viewType="Telerik.Sitefinity.DynamicModules.Web.UI.Backend.DynamicContentMasterGridView" viewName="Config changeBackendList" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.MasterGridViewElement, Telerik.Sitefinity">
|
||||||
|
<decisionScreens>
|
||||||
|
<add decisionType="NoItemsExist" displayed="False" messageText="No config changes have been created yet" messageType="Neutral" name="NoItemsExistScreen">
|
||||||
|
<actions>
|
||||||
|
<add commandName="create" commandButtonType="Create" isFilter="False" permissionSet="Configchangetests-ConfigChange" actionName="Create" relatedSecuredObjectTypeName="Telerik.Sitefinity.DynamicModules.Builder.Model.DynamicModuleType" relatedSecuredObjectId="95f17af4-12b4-491f-bfbd-f4cf8c02c0b0" cssClass="sfCreateItem" text="Create a config change" wrapperTagKey="Unknown" isSeparator="False" name="Create" />
|
||||||
|
</actions>
|
||||||
|
</add>
|
||||||
|
</decisionScreens>
|
||||||
|
<dialogs>
|
||||||
|
<add name="ContentViewInsertDialog" openOnCommand="create" height="100%" width="100%" initialBehaviors="Maximize" behaviors="None" autoSizeBehaviors="Default" isfullscreen="False" visiblestatusbar="False" visibletitlebar="False" params="?ControlDefinitionName=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition&ViewName=Config changeBackendInsertView" ismodal="False" destroyOnClose="False" ReloadOnShow="False" cssclass="sfMaximizedWindow" id="ContentViewInsertDialog on create" />
|
||||||
|
<add name="ContentViewEditDialog" openOnCommand="edit" height="100%" width="100%" initialBehaviors="Maximize" behaviors="None" autoSizeBehaviors="Default" isfullscreen="False" visiblestatusbar="False" visibletitlebar="False" params="?ControlDefinitionName=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition&ViewName=Config changeBackendEditView&Id={{Id}}" ismodal="False" destroyOnClose="False" ReloadOnShow="False" cssclass="sfMaximizedWindow" id="ContentViewEditDialog on edit" />
|
||||||
|
<add name="ContentViewEditDialog" openOnCommand="preview" height="100%" width="100%" initialBehaviors="Maximize" behaviors="None" autoSizeBehaviors="Default" isfullscreen="False" visiblestatusbar="False" visibletitlebar="False" params="?ControlDefinitionName=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChangeBackendDefinition&ViewName=Config changeBackendPreviewView" ismodal="False" destroyOnClose="False" ReloadOnShow="False" cssclass="sfMaximizedWindow" id="ContentViewEditDialog on preview" />
|
||||||
|
<add name="ModulePermissionsDialog" openOnCommand="permissions" height="100%" width="100%" initialBehaviors="Maximize" behaviors="None" autoSizeBehaviors="Default" isfullscreen="False" visiblestatusbar="False" visibletitlebar="False" params="?moduleName=ModuleBuilder&typeName=Telerik.Sitefinity.DynamicModules.Builder.Model.DynamicModuleType&securedObjectId=95f17af4-12b4-491f-bfbd-f4cf8c02c0b0&backLabelText=Back to items&title=Permissions&permissionSetName=Configchangetests-ConfigChange" ismodal="False" destroyOnClose="False" ReloadOnShow="False" cssclass="sfMaximizedWindow" id="ModulePermissionsDialog on permissions" />
|
||||||
|
</dialogs>
|
||||||
|
<viewModes>
|
||||||
|
<add EnableDragAndDrop="False" EnableInitialExpanding="False" Name="Grid" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.GridViewModeElement, Telerik.Sitefinity">
|
||||||
|
<columns>
|
||||||
|
<add clientTemplate="<a sys:href=’javascript:void(0);’ sys:class="{{ ‘sf_binderCommand_edit sfItemTitle sf’ + Lifecycle.WorkflowStatus.replace(‘ ‘,”).toLowerCase()}}"><strong>{{Title}}</strong><span class=’sfStatusLocation’>{{Lifecycle.WorkflowStatus}}</span></a>" headerCssClass="sfTitleCol" headerText="Title" itemCssClass="sfTitleCol" width="0" disableSorting="False" name="Title" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DataColumnElement, Telerik.Sitefinity" />
|
||||||
|
<add headerCssClass="sfMoreActions" headerText="Actions" itemCssClass="sfMoreActions" width="0" disableSorting="False" name="Actions" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.ActionMenuColumnElement, Telerik.Sitefinity">
|
||||||
|
<mainAction commandButtonType="Standard" isFilter="False" wrapperTagKey="Unknown" isSeparator="False" />
|
||||||
|
<menuItems>
|
||||||
|
<menuItem commandName="delete" commandButtonType="Standard" isFilter="False" cssClass="sfDeleteItm" text="Delete" wrapperTagKey="Li" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="Delete" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</menuItems>
|
||||||
|
</add>
|
||||||
|
<add clientTemplate="<span>{{Author}}</span>" resourceClassId="Labels" headerCssClass="sfAuthor" headerText="Author" itemCssClass="sfAuthor" width="0" disableSorting="False" name="Author" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DataColumnElement, Telerik.Sitefinity" />
|
||||||
|
<add clientTemplate="<span>{{ (PublicationDate) ? PublicationDate.sitefinityLocaleFormat(‘dd MMM, yyyy hh:mm:ss’): ‘-‘ }}</span>" resourceClassId="ModuleBuilderResources" headerCssClass="sfDateAndHour" headerText="PublicationDate" itemCssClass="sfDateAndHour" width="0" disableSorting="False" name="PublicationDate" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DataColumnElement, Telerik.Sitefinity" />
|
||||||
|
</columns>
|
||||||
|
</add>
|
||||||
|
</viewModes>
|
||||||
|
<links>
|
||||||
|
<add navigateUrl="[node:70aef8e7-1bf8-49d0-a7c9-21b90fb9c7a6]/fa2e4a73-929b-4050-a4ac-6030808f41cf" commandName="goBackToContentTypes" name="NavigateToContentTypesLink" />
|
||||||
|
</links>
|
||||||
|
<toolbar wrapperTagKey="Unknown">
|
||||||
|
<sections>
|
||||||
|
<section titleWrapperTagKey="Unknown" wrapperTagKey="Unknown" visible="True" name="toolbar">
|
||||||
|
<items>
|
||||||
|
<item commandName="create" commandButtonType="Create" isFilter="False" permissionSet="Configchangetests-ConfigChange" actionName="Create" relatedSecuredObjectTypeName="Telerik.Sitefinity.DynamicModules.Builder.Model.DynamicModuleType" relatedSecuredObjectId="95f17af4-12b4-491f-bfbd-f4cf8c02c0b0" cssClass="sfMainAction" text="Create a Config change" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="CreateItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="groupDelete" commandButtonType="Standard" isFilter="False" permissionSet="Configchangetests-ConfigChange" actionName="Delete" relatedSecuredObjectTypeName="Telerik.Sitefinity.DynamicModules.Builder.Model.DynamicModuleType" relatedSecuredObjectId="95f17af4-12b4-491f-bfbd-f4cf8c02c0b0" text="Delete" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="DeleteItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item text="More actions" wrapperTagKey="Li" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.ActionMenuWidget" isSeparator="False" name="MoreActionsItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.ActionMenuWidgetElement, Telerik.Sitefinity">
|
||||||
|
<mainAction commandButtonType="Standard" isFilter="False" wrapperTagKey="Unknown" isSeparator="False" />
|
||||||
|
<menuItems>
|
||||||
|
<item commandName="groupPublish" commandButtonType="Standard" isFilter="False" cssClass="sfPublishItm" text="Publish" wrapperTagKey="Unknown" isSeparator="False" name="PublishItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="groupUnpublish" commandButtonType="Standard" isFilter="False" cssClass="sfUnpublishItm" text="Unpublish" wrapperTagKey="Unknown" isSeparator="False" name="UnpublishItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</menuItems>
|
||||||
|
</item>
|
||||||
|
<item persistentTypeToSearch="Telerik.Sitefinity.GenericContent.Model.Content" mode="NotSet" commandName="search" commandButtonType="Standard" isFilter="False" text="Search" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.SearchWidget" isSeparator="False" name="SearchItemWidget" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.SearchWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</items>
|
||||||
|
</section>
|
||||||
|
</sections>
|
||||||
|
</toolbar>
|
||||||
|
<sidebar title="Manage Config changes" wrapperTagKey="Unknown">
|
||||||
|
<sections>
|
||||||
|
<section title="Filter Config change" titleWrapperTagKey="Unknown" wrapperTagKey="Unknown" cssClass="sfFirst sfWidgetsList sfSeparator sfModules" visible="True" name="Filter">
|
||||||
|
<items>
|
||||||
|
<item commandName="showAllItems" commandButtonType="SimpleLinkButton" isFilter="False" buttonCssClass="sfSel" text="All Config changes" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="AllItems" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="showMyItems" commandButtonType="SimpleLinkButton" isFilter="False" text="My Config change" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="MyItems" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</items>
|
||||||
|
</section>
|
||||||
|
<section title="Settings" titleWrapperTagKey="Unknown" wrapperTagKey="Unknown" cssClass="sfWidgetsList sfSettings" resourceClassId="ModuleBuilderResources" visible="True" name="Settings">
|
||||||
|
<items>
|
||||||
|
<item commandName="goBackToContentTypes" commandButtonType="SimpleLinkButton" isFilter="False" text="Content types" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="NavigateToContentTypes" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="permissions" commandButtonType="SimpleLinkButton" isFilter="False" text="Permissions" resourceclassid="ModuleBuilderResources" wrapperTagKey="Unknown" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="Permissions" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</items>
|
||||||
|
</section>
|
||||||
|
</sections>
|
||||||
|
</sidebar>
|
||||||
|
<contextBar wrapperTagKey="Unknown" />
|
||||||
|
<scripts>
|
||||||
|
<script scriptLocation="Telerik.Sitefinity.Resources.Scripts.jquery.shorten.js, Telerik.Sitefinity.Resources" />
|
||||||
|
<script loadMethodName="OnModuleMasterViewLoaded" scriptLocation="Telerik.Sitefinity.DynamicModules.Web.UI.Backend.Script.MasterGridViewGeneratorExtensions.js, Telerik.Sitefinity" />
|
||||||
|
</scripts>
|
||||||
|
<commentsSettingsDefinition postRights="None" />
|
||||||
|
</view>
|
||||||
|
<view showTopToolbar="True" webServiceBaseUrl="~/Sitefinity/Services/DynamicModules/Data.svc/?itemType=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" showNavigation="False" createBlankItem="True" unlockDetailItemOnExit="True" isToRenderTranslationView="False" doNotUseContentItemContext="False" multilingualMode="Automatic" showSections="True" masterPageId="00000000-0000-0000-0000-000000000000" dataItemId="00000000-0000-0000-0000-000000000000" enableSocialSharing="False" displayMode="Write" useWorkflow="True" title="Create a Config change" viewType="Telerik.Sitefinity.DynamicModules.Web.UI.Backend.DynamicContentDetailFormView" viewName="Config changeBackendInsertView" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DetailFormViewElement, Telerik.Sitefinity">
|
||||||
|
<toolbar wrapperTagKey="Unknown">
|
||||||
|
<sections>
|
||||||
|
<section titleWrapperTagKey="Unknown" wrapperTagKey="Div" cssClass="sfWorkflowMenuWrp" visible="True" name="BackendForm">
|
||||||
|
<items>
|
||||||
|
<item commandName="save" commandButtonType="Save" isFilter="False" text="Create Config change" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="SaveChangesWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="cancel" commandButtonType="Cancel" isFilter="False" text="Back to Press Releases" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="CancelWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="preview" commandButtonType="Standard" isFilter="False" text="Preview" resourceclassid="Labels" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="PreviewWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</items>
|
||||||
|
</section>
|
||||||
|
</sections>
|
||||||
|
</toolbar>
|
||||||
|
<sections>
|
||||||
|
<sections cssClass="sfFirstForm" wrapperTag="Div" isHiddenInTranslationMode="False" name="MainSection">
|
||||||
|
<fields>
|
||||||
|
<field rows="1" id="TitleControl" dataFieldName="Title" displayMode="Write" wrapperTag="Li" title="Title" fieldType="Telerik.Sitefinity.Web.UI.Fields.TextField" cssClass="sfFormSeparator" fieldName="Title" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.TextFieldDefinitionElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="0" minLength="0" required="True" maxLengthViolationMessage="The input is too long" messageCssClass="sfError" minLengthViolationMessage="The input is too short" requiredViolationMessage="This field is required!" validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</sections>
|
||||||
|
<sections cssClass="sfExpandableForm" title="More Options" wrapperTag="Div" isHiddenInTranslationMode="False" name="MoreOptions">
|
||||||
|
<fields>
|
||||||
|
<field regularExpressionFilter="[^\p{L}\-\!\$\(\)\=\@\d_\’\.]+|\.+$" replaceWith="-" mirroredControlId="TitleControl" enableChangeButton="True" toLower="True" trim="True" rows="1" id="UrlNameFieldControl" dataFieldName="UrlName.PersistedValue" displayMode="Write" wrapperTag="Li" title="UrlNameTitle" example="UrlNameExample" fieldType="Telerik.Sitefinity.Web.UI.Fields.MirrorTextField" resourceClassId="ModuleBuilderResources" cssClass="sfFormSeparator" fieldName="UrlName" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.MirrorTextFieldElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="-1" minLength="-1" regularExpression="^[\p{L}\-\!\$\(\)\=\@\d_\’~\.]*[\p{L}\-\!\$\(\)\=\@\d_\’~]+$" required="True" messageCssClass="sfError" regularExpressionViolationMessage="The URL contains invalid symbols." requiredViolationMessage="Url name cannot be empty." validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="False" />
|
||||||
|
</sections>
|
||||||
|
<sections cssClass="sfItemReadOnlyInfo" wrapperTag="Div" isHiddenInTranslationMode="False" name="SidebarSection">
|
||||||
|
<fields>
|
||||||
|
<field displayMode="Write" wrapperTag="Li" fieldType="Telerik.Sitefinity.Web.UI.Fields.ContentWorkflowStatusInfoField" fieldName="ItemWorkflowStatusInfoField" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.ContentWorkflowStatusInfoFieldElement, Telerik.Sitefinity">
|
||||||
|
<validator expectedFormat="None" maxLength="-1" minLength="-1" required="False" validateIfInvisible="True" />
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</sections>
|
||||||
|
</sections>
|
||||||
|
<commentsSettingsDefinition postRights="None" />
|
||||||
|
</view>
|
||||||
|
<view showTopToolbar="False" webServiceBaseUrl="~/Sitefinity/Services/DynamicModules/Data.svc/?itemType=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" showNavigation="True" createBlankItem="True" unlockDetailItemOnExit="True" doNotUseContentItemContext="False" multilingualMode="Automatic" showSections="True" masterPageId="00000000-0000-0000-0000-000000000000" dataItemId="00000000-0000-0000-0000-000000000000" enableSocialSharing="False" displayMode="Read" useWorkflow="False" title="Preview a Config change" viewType="Telerik.Sitefinity.DynamicModules.Web.UI.Backend.DynamicContentDetailFormView" viewName="Config changeBackendPreviewView" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DetailFormViewElement, Telerik.Sitefinity">
|
||||||
|
<toolbar wrapperTagKey="Unknown" />
|
||||||
|
<sections>
|
||||||
|
<sections cssClass="sfFirstForm" wrapperTag="Div" isHiddenInTranslationMode="False" name="MainSection">
|
||||||
|
<fields>
|
||||||
|
<field rows="1" id="TitleControl" dataFieldName="Title" displayMode="Read" wrapperTag="Li" title="Title" fieldType="Telerik.Sitefinity.Web.UI.Fields.TextField" cssClass="sfFormSeparator" fieldName="Title" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.TextFieldDefinitionElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="0" minLength="0" required="True" maxLengthViolationMessage="The input is too long" messageCssClass="sfError" minLengthViolationMessage="The input is too short" requiredViolationMessage="This field is required!" validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</sections>
|
||||||
|
<sections cssClass="sfExpandableForm" title="More Options" wrapperTag="Div" isHiddenInTranslationMode="False" name="MoreOptions">
|
||||||
|
<fields>
|
||||||
|
<field regularExpressionFilter="[^\p{L}\-\!\$\(\)\=\@\d_\’\.]+|\.+$" replaceWith="-" mirroredControlId="TitleControl" enableChangeButton="True" toLower="True" trim="True" rows="1" id="UrlNameFieldControl" dataFieldName="UrlName.PersistedValue" displayMode="Read" wrapperTag="Li" title="UrlNameTitle" example="UrlNameExample" fieldType="Telerik.Sitefinity.Web.UI.Fields.MirrorTextField" resourceClassId="ModuleBuilderResources" cssClass="sfFormSeparator" fieldName="UrlName" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.MirrorTextFieldElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="-1" minLength="-1" regularExpression="^[\p{L}\-\!\$\(\)\=\@\d_\’~\.]*[\p{L}\-\!\$\(\)\=\@\d_\’~]+$" required="True" messageCssClass="sfError" regularExpressionViolationMessage="The URL contains invalid symbols." requiredViolationMessage="Url name cannot be empty." validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="False" />
|
||||||
|
</sections>
|
||||||
|
</sections>
|
||||||
|
<commentsSettingsDefinition postRights="None" />
|
||||||
|
</view>
|
||||||
|
<view showTopToolbar="True" webServiceBaseUrl="~/Sitefinity/Services/DynamicModules/Data.svc/?itemType=Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" showNavigation="False" createBlankItem="True" unlockDetailItemOnExit="True" isToRenderTranslationView="False" doNotUseContentItemContext="False" multilingualMode="Automatic" showSections="True" masterPageId="00000000-0000-0000-0000-000000000000" dataItemId="00000000-0000-0000-0000-000000000000" enableSocialSharing="False" displayMode="Write" useWorkflow="True" title="Edit a Config change" viewType="Telerik.Sitefinity.DynamicModules.Web.UI.Backend.DynamicContentDetailFormView" viewName="Config changeBackendEditView" type:this="Telerik.Sitefinity.Web.UI.ContentUI.Views.Backend.Master.Config.DetailFormViewElement, Telerik.Sitefinity">
|
||||||
|
<toolbar wrapperTagKey="Unknown">
|
||||||
|
<sections>
|
||||||
|
<section titleWrapperTagKey="Unknown" wrapperTagKey="Div" cssClass="sfWorkflowMenuWrp" visible="True" name="BackendForm">
|
||||||
|
<items>
|
||||||
|
<item commandName="save" commandButtonType="Save" isFilter="False" text="Create Config change" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="SaveChangesWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="cancel" commandButtonType="Cancel" isFilter="False" text="Back to Press Releases" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="CancelWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
<item commandName="preview" commandButtonType="Standard" isFilter="False" text="Preview" resourceclassid="Labels" wrapperTagKey="Span" widgetType="Telerik.Sitefinity.Web.UI.Backend.Elements.Widgets.CommandWidget" isSeparator="False" name="PreviewWidgetElement" type:this="Telerik.Sitefinity.Web.UI.Backend.Elements.Config.CommandWidgetElement, Telerik.Sitefinity" />
|
||||||
|
</items>
|
||||||
|
</section>
|
||||||
|
</sections>
|
||||||
|
</toolbar>
|
||||||
|
<sections>
|
||||||
|
<sections cssClass="sfFirstForm" wrapperTag="Div" isHiddenInTranslationMode="False" name="MainSection">
|
||||||
|
<fields>
|
||||||
|
<field rows="1" id="TitleControl" dataFieldName="Title" displayMode="Write" wrapperTag="Li" title="Title" fieldType="Telerik.Sitefinity.Web.UI.Fields.TextField" cssClass="sfFormSeparator" fieldName="Title" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.TextFieldDefinitionElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="0" minLength="0" required="True" maxLengthViolationMessage="The input is too long" messageCssClass="sfError" minLengthViolationMessage="The input is too short" requiredViolationMessage="This field is required!" validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</sections>
|
||||||
|
<sections cssClass="sfExpandableForm" title="More Options" wrapperTag="Div" isHiddenInTranslationMode="False" name="MoreOptions">
|
||||||
|
<fields>
|
||||||
|
<field regularExpressionFilter="[^\p{L}\-\!\$\(\)\=\@\d_\’\.]+|\.+$" replaceWith="-" mirroredControlId="TitleControl" enableChangeButton="True" toLower="True" trim="True" rows="1" id="UrlNameFieldControl" dataFieldName="UrlName.PersistedValue" displayMode="Write" wrapperTag="Li" title="UrlNameTitle" example="UrlNameExample" fieldType="Telerik.Sitefinity.Web.UI.Fields.MirrorTextField" resourceClassId="ModuleBuilderResources" cssClass="sfFormSeparator" fieldName="UrlName" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.MirrorTextFieldElement, Telerik.Sitefinity">
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
<validator expectedFormat="None" maxLength="-1" minLength="-1" regularExpression="^[\p{L}\-\!\$\(\)\=\@\d_\’~\.]*[\p{L}\-\!\$\(\)\=\@\d_\’~]+$" required="True" messageCssClass="sfError" regularExpressionViolationMessage="The URL contains invalid symbols." requiredViolationMessage="Url name cannot be empty." validateIfInvisible="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="False" />
|
||||||
|
</sections>
|
||||||
|
<sections cssClass="sfItemReadOnlyInfo" wrapperTag="Div" isHiddenInTranslationMode="False" name="SidebarSection">
|
||||||
|
<fields>
|
||||||
|
<field displayMode="Write" wrapperTag="Li" fieldType="Telerik.Sitefinity.Web.UI.Fields.ContentWorkflowStatusInfoField" fieldName="ItemWorkflowStatusInfoField" type:this="Telerik.Sitefinity.Web.UI.Fields.Config.ContentWorkflowStatusInfoFieldElement, Telerik.Sitefinity">
|
||||||
|
<validator expectedFormat="None" maxLength="-1" minLength="-1" required="False" validateIfInvisible="True" />
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<expandableDefinition expanded="True" />
|
||||||
|
</sections>
|
||||||
|
</sections>
|
||||||
|
<commentsSettingsDefinition postRights="None" />
|
||||||
|
</view>
|
||||||
|
</views>
|
||||||
|
</contentViewControl>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## SecurityConfig.config
|
||||||
|
A permission element is added underneath the permissions node. Example:
|
||||||
|
{% highlight xml %}
|
||||||
|
<permission title="Config changes permissions" description="Represents the most common application security permissions." loginUrl="~/Sitefinity/Login" ajaxLoginUrl="~/Sitefinity/Login/Ajax" name="Configchangetests-ConfigChange">
|
||||||
|
<actions>
|
||||||
|
<add title="View Config changes" description="Allows or denies viewing Config changes." type="View" name="View" />
|
||||||
|
<add title="Create Config changes" description="Allows or denies the creation of new Config changes." type="Create" name="Create" />
|
||||||
|
<add title="Modify Config changes" description="Allows or denies changes to existing Config changes." type="Modify" name="Modify" />
|
||||||
|
<add title="Delete Config changes" description="Allows or denies deleting Config changes." type="Delete" name="Delete" />
|
||||||
|
<add title="Change a Config changes permissions" description="Allows or denies changing the permissions of Config changes." type="ChangePermissions" name="ChangePermissions" />
|
||||||
|
</actions>
|
||||||
|
</permission>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## ToolboxesConfig.config
|
||||||
|
A tool is added in the tools section. Example:
|
||||||
|
{% highlight xml %}
|
||||||
|
<add enabled="True" type="Telerik.Sitefinity.DynamicModules.Web.UI.Frontend.DynamicContentView, Telerik.Sitefinity" title="Config changes" cssClass="sfNewsViewIcn" moduleName="Config change tests" DynamicContentTypeName="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" DefaultMasterTemplateKey="844f4eff-9a33-4799-84e5-2973d7a3db9b" DefaultDetailTemplateKey="7fd83546-0785-4050-998f-06428e7c6fa1" visibilityMode="None" name="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## WorkflowConfig.config
|
||||||
|
A workflow type is added in the workflowTypes section. Example:
|
||||||
|
{% highlight xml %}
|
||||||
|
<add title="Config change" moduleName="Config change tests" contentType="Telerik.Sitefinity.DynamicTypes.Model.Configchangetests.ConfigChange" />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## Deactivation
|
||||||
|
No configuration changes are made in the Sitefinity configuration files when you just deactivate a dynamic module with Module Builder.
|
||||||
|
|
||||||
|
Interestingly, when you delete a dynamic module, all of the changes mentioned above are completely reverted EXCEPT the ContentViewConfig.config file. It keeps the config:link element that was added when you added and activated the dynamic module originally. This is curious as it points to a configuration element in DynamicModulesConfig.config that is removed when you delete the dynamic module.
|
||||||
32
src/posts/2012/10/12/custom-error-pages-with-sitefinity.md
Normal file
32
src/posts/2012/10/12/custom-error-pages-with-sitefinity.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Custom Error Pages with Sitefinity"
|
||||||
|
date: 2012-10-12
|
||||||
|
description: "Custom Error Pages with Sitefinity"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity,.net]
|
||||||
|
---
|
||||||
|
I had a considerably hard time today getting just a simple 404 error page (custom) to show up in all cases for a Sitefinity installation. It probably has nothing to do with Sitefinity and a lot more to do with my lack of experience doing this simple task. But, anyhow, here are my findings.
|
||||||
|
|
||||||
|
You have to setup the error pages in two places in the application’s web.config to cover all of your bases.
|
||||||
|
|
||||||
|
## system.web/customErrors
|
||||||
|
You have to setup your error pages in the system.web/customErrors section of your web.config to cover calls to non-existent *.aspx pages (and probably other ASP.NET-handled pages as well). My customErrors section looks like this. It will be expanded to cover other error codes.
|
||||||
|
{% highlight xml %}
|
||||||
|
<customErrors mode="On" redirectMode="ResponseRewrite">
|
||||||
|
<error statusCode="404" redirect="~/Static/404.htm"/>
|
||||||
|
</customErrors>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## system.webServer/httpErrors
|
||||||
|
The next section you need is the system.webServer/httpErrors section. I never knew this section exists, but it corresponds to the Errors panel in IIS Manager 7.5 for your website. This section covers request for non-existing folders and static files (like /bogusFolder or /bogusFile.htm). Mine looks like this and it will likewise be expanded for other error codes.
|
||||||
|
{% highlight xml %}
|
||||||
|
<httpErrors errorMode="Custom" defaultResponseMode="File">
|
||||||
|
<remove statusCode="404"/>
|
||||||
|
<error statusCode="404" path="Static\404.htm"/>
|
||||||
|
</httpErrors>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Because IIS has default settings for this stuff, you need to use the remove element before adding your own error messages. You can find more specifics on this configuration element here: [http://www.iis.net/configreference/system.webserver/httperrors](http://www.iis.net/configreference/system.webserver/httperrors).
|
||||||
|
|
||||||
|
You have other options than the defaultResponseMode=”File” for the httpErrors section. See the link above for more information about this. You can redirect to *.aspx files and stuff for more dynamically handled pages. I wanted very simple, static HTML pages to ensure maximum possibility of the page rendering and displaying a legible error to the user.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Indexing Taxonomy Fields For Search in Sitefinity 5.1"
|
||||||
|
date: 2012-10-19
|
||||||
|
description: "Indexing Taxonomy Fields For Search in Sitefinity 5.1"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
We recently ran into a rather surprising issue with a Sitefinity 5.1 installation. We wanted a pretty simple feature: when searching and using Lucene, we wanted the values of any taxonomy fields to be indexed with Sitefinity’s Lucene back-end so that items could be searched by their attached taxonomy. This is apparently impossible with the basic installation because you can only search short text and long text fields.
|
||||||
|
|
||||||
|
So, I began on a quest to implement this somehow and ran into another wall: the Sitefinity documentation on their search API is horrendously non-existent. We mostly had to rely on forum posts and decompiling Telerik assemblies to see how stuff worked. But, this did eventually lead to a solid solution.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
It took me about 13 hours of research to figure out what to do and then just about an hour to actually implement. So, the good news is that it’s a pretty simple solution!
|
||||||
|
|
||||||
|
When you create a search index in Sitefinity’s back-end, you can add a comma-separated list of extra fields to index. Sitefinity indexes a standard set (like Title). But, if you create custom dynamic modules with other fields you want to index you have to add them in this Additional Fields box. Here here:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Looking through the de-compiled code for Sitefinity’s SearchModule, I found that it registered a special outbound pipe that updates the Lucene indexes (not directly, but through Sitefinity’s abstraction, it appears) when you publish an item. It also registered various Translator classes with PipeTranslatorFactory.RegisterTranslator. Digging through the code in that SearchIndexOutboundPipe and the various translator classes it registered, I finally found that when the SearchIndexOutboundPipe runs, it reads that comma-separated list of additional fields from your index and ties them to the ConcatenationTranslator through some type of mapping. It looked like these translators were being used by the SearchModule to translate content values into index-able values for the Lucene indexing system.
|
||||||
|
|
||||||
|
Turns out, I was right. With some more trial and error from that point on, I came up with this solution.
|
||||||
|
|
||||||
|
Override ConcatenationTranslator
|
||||||
|
First of all, you need to create a new class that inherits from ConcatenationTranslator. This is a public class in the Telerik.Sitefinity.Publishing.Translators namespace. I called my class CustomConcatenationTranslator. IN the base class, the Translate method turns an array of object values into a space-separated string. I’ll show the code in a minute, but the first thing you need to do after creating the class is register it with the pipeline. In Global.asax.cs, add these lines (around any other code you have there):
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
protected void Application_Start(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Bootstrapper.Initialized += Bootstrapper_Initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Bootstrapper_Initialized(object sender, Telerik.Sitefinity.Data.ExecutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.CommandName == "Bootstrapped")
|
||||||
|
{
|
||||||
|
PipeTranslatorFactory.RegisterTranslator(new CustomConcatenationTranslator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
You don’t have to unregistered the existing ConcatenationTranslator. Your registration will replace it as long as you don’t rename the translator by overriding it’s Name property.
|
||||||
|
|
||||||
|
What I wanted to do was check if the data being translated was a TrackedList<Guid> type. This is what your taxonomy fields are in code. Here is my code for the CustomConcatenationTranslator class.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class CustomConcatenationTranslator : ConcatenationTranslator
|
||||||
|
{
|
||||||
|
private readonly TaxonomyManager _taxonomyManager;
|
||||||
|
|
||||||
|
public CustomConcatenationTranslator()
|
||||||
|
{
|
||||||
|
_taxonomyManager = TaxonomyManager.GetManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object Translate(object[] data, IDictionary<string, string> translationSettings)
|
||||||
|
{
|
||||||
|
if (data.Length <= 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder concatedStr = new StringBuilder();
|
||||||
|
ConcatValues(data, concatedStr);
|
||||||
|
return concatedStr.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTrackedList<T>(object data)
|
||||||
|
{
|
||||||
|
return data is TrackedList<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConcatValues(object[] data, StringBuilder concatedStr)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < data.Length; i++)
|
||||||
|
{
|
||||||
|
string str;
|
||||||
|
if (IsTrackedList<Guid>(data[i]))
|
||||||
|
{
|
||||||
|
str = TranslatedTaxonomies(data, i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str = GetString(data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
concatedStr.Append(str);
|
||||||
|
if (i + 1 < data.Length)
|
||||||
|
{
|
||||||
|
concatedStr.Append(‘ ‘);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string TranslatedTaxonomies(object[] data, int i)
|
||||||
|
{
|
||||||
|
List<string> taxNames = new List<string>();
|
||||||
|
foreach (Guid guid in ((TrackedList<Guid>)data[i]))
|
||||||
|
{
|
||||||
|
var taxon = _taxonomyManager.GetTaxon(guid);
|
||||||
|
taxNames.Add(taxon.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", taxNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
It should be pretty simple to follow. Basically, if I find a data value is a TrackedList<Guid>, I get the Taxon items with that Guid ID, get the name of that Taxon and then concatenate those values, separated by a space. If the data object is anything other than a TrackedList<Guid>, I used the default ConcatenationTranslator behavior.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
That’s really all you need to do. Just go into your Sitefinity back-end, re-index your search index and you can now search your content by any taxonomy it has attached to it.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Setting Selected Items on Sitefinity 5.1 PagesSelector in Javascript"
|
||||||
|
date: 2012-10-25
|
||||||
|
description: "Setting Selected Items on Sitefinity 5.1 PagesSelector in Javascript"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
Today I nearly tore my hair out trying to get set the selected items (having a list of GUIDs) on the PagesSelector control in Sitefinity through the Javascript API to the control.
|
||||||
|
|
||||||
|
You are supposed to be able to use the set_selectedItemIds method to do this, but it doesn’t work. Nothing happens after you call this method.
|
||||||
|
|
||||||
|
After doing some debugging through Sitefinity’s javascript, I found a bug which, when fixed, causes the set_selectedItemIds method to work as expected.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
I am using a WidgetDesigner in which I have the PagesSelector control. My goal is pretty simple: I want the content editors to be able to select a set of pages for which to show links in the footer of their website. When they edit the footer widget, the PagesSelector should show up, let them choose pages (showing the currently selected ones) and let them save those selections.
|
||||||
|
|
||||||
|
Everything works fine. Except, once I save a selection in the PagesSelector, on subsequent edits, the currently selected items would not show up, as described above.
|
||||||
|
|
||||||
|
Here is the bug I found in the Sitefinity code. The set_selectedItemIds function is really simple. It looks like this:
|
||||||
|
{% highlight javascript %}
|
||||||
|
set_selectedItemIds: function(ids) {
|
||||||
|
this._selectedItemIds = ids;
|
||||||
|
this._updateGridSelection();
|
||||||
|
this._updateTreeSelection();
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
It updates a property (_selectedItemIds) and then updates the grid (not sure what that is) and then the tree (of page nodes). The _updateTreeSelection method is what we’re looking for–this is where the bug is. The method looks like this:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
_updateTreeSelection: function () {
|
||||||
|
if (this._treeIsBound == false) {
|
||||||
|
this._treeMustBeUpdated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._treeMustBeUpdated = false;
|
||||||
|
var tree = this.get_itemsTree();
|
||||||
|
var binder = tree.getBinder();
|
||||||
|
if (this._selectedItemId) {
|
||||||
|
binder.setSelectedValues([this._selectedItemId], true, true);
|
||||||
|
} else {
|
||||||
|
var selectedItems = tree.get_selectedItems();
|
||||||
|
if (selectedItems) {
|
||||||
|
binder.clearSelection();
|
||||||
|
binder.setSelectedItems(selectedItems, true, true);
|
||||||
|
} else {
|
||||||
|
if (this._selectedItemIds) {
|
||||||
|
binder.setSelectedValues(this._selectedItemIds, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._raiseSelectionApplied(this, {});
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
The problem is on line 13. tree.get_selectedItems() returns an empty array when there are no items selected. So, the expression if (selectedItems) { } will always be true–but we want to get to the else statement, as you can see, to set the selection from the this._selectedItemIds array.
|
||||||
|
|
||||||
|
The beauty (and danger!) of javascript is that you can easily fix something like this in your own code by simply redefining their method. Here is my code that fixes this bug:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
var wrapper = this.get_pagesSelector();
|
||||||
|
var selector = this.get_pagesSelector().get_pageSelector();
|
||||||
|
var selecting = false;
|
||||||
|
wrapper.add_selectionApplied(function () {
|
||||||
|
if (selecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selecting = true;
|
||||||
|
var pageIds = controlData.FooterPages;
|
||||||
|
if (pageIds) {
|
||||||
|
var pages = pageIds.split(",");
|
||||||
|
var oldMethod = selector._updateTreeSelection;
|
||||||
|
selector._updateTreeSelection = function () {
|
||||||
|
if (this._treeIsBound == false) {
|
||||||
|
this._treeMustBeUpdated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._treeMustBeUpdated = false;
|
||||||
|
var tree = this.get_itemsTree();
|
||||||
|
var binder = tree.getBinder();
|
||||||
|
if (this._selectedItemId) {
|
||||||
|
binder.setSelectedValues([this._selectedItemId], true, true);
|
||||||
|
} else {
|
||||||
|
var selectedItems = tree.get_selectedItems();
|
||||||
|
if (selectedItems && selectedItems.length > 0) {
|
||||||
|
binder.clearSelection();
|
||||||
|
binder.setSelectedItems(selectedItems, true, true);
|
||||||
|
} else {
|
||||||
|
if (this._selectedItemIds) {
|
||||||
|
binder.setSelectedValues(this._selectedItemIds, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._raiseSelectionApplied(this, {});
|
||||||
|
};
|
||||||
|
selector.set_selectedItemIds(pages);
|
||||||
|
selector._updateTreeSelection = oldMethod;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
First, I attach to the selectionApplied method. If you try to do the above code before that is done, your selection will just be overridden (or just not work–not sure which). This event will get called when you call set_selectedItemIds. To avoid an infinite loop, then, I use the selecting flag at the top of the event function I am attaching.
|
||||||
|
|
||||||
|
Second, I parse out my ids into an array.
|
||||||
|
|
||||||
|
Third, I save the old version of the _updateTreeSelection method and then override it with my own. Notice, the fix on line 13 of that method. I have added ” && selectedItems.length > 0″.
|
||||||
|
|
||||||
|
Fourth, now that I have a fixed method, I call selector.set_selectedItemIds, passing in my array of ids and it works!
|
||||||
|
|
||||||
|
Lastly, I change the method back to the original (just in case the seemingly faulty if expression is what they intended in other situations).
|
||||||
|
|
||||||
|
Hope that helps!
|
||||||
36
src/posts/2013/01/15/linq-to-ektron-search-library.md
Normal file
36
src/posts/2013/01/15/linq-to-ektron-search-library.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "LINQ to Ektron Search Library"
|
||||||
|
date: 2013-01-15
|
||||||
|
description: "LINQ to Ektron Search Library"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ektron]
|
||||||
|
---
|
||||||
|
I am currently working on a project at VML that will make use of Ektron’s 3-tier architecture. It will be an MVC 4.0 site that basically uses an existing Ektron site as a datasource. It pulls in various Ektron content and then feeds it into the MVC application (a mobile version of the Ektron site).
|
||||||
|
|
||||||
|
I wanted to take time on this project to make everything “right”. I am using MVC as my presentation layer, I have a distinct business logic layer and underneath that I have a nice data layer. This data layer is what is interacting with Ektron. The data model for the application is defined separately through a set of interfaces that describe different data objects. The presentation and business logic layers know nothing about the concrete implementations of these interfaces. So, my project structure looks like this:
|
||||||
|
|
||||||
|
- Project.Web – my MVC application
|
||||||
|
- Project.Data – the set of interfaces that define my data model and repositories
|
||||||
|
- Project.Data.Ektron – the implementation of the Project.Data interfaces with Ektron (data model and repositories)
|
||||||
|
- Project.BusinessLayer – the service layer that sits on top of Project.Data.Ektron (with no direct knowledge of it)
|
||||||
|
I tie it all neatly together with dependency injection using Autofac (that’s how my business layer doesn’t need to know about Project.Data.Ektron).
|
||||||
|
|
||||||
|
I thought about a few designs for how my repositories would receive queries for data. Of course, nothing beats LINQ for this purpose. The problem is, Ektron doesn’t implement anything like LINQ-to-Ektron to query its data. So, what to do?
|
||||||
|
|
||||||
|
I really didn’t want to move away from LINQ. Anything else was going to be too rigid and cumbersome to use. I started looking into my own implementation of LINQ-to-Ektron. Ektron already implements a LINQ-like expression tree search via it’s AdvancedSearchCriteria search API. So, my thinking was to take LINQ queries and translate them into those AdvancedSearchCriteria expressions.
|
||||||
|
|
||||||
|
As I started to look into what it took to create a LINQ QueryProvider, I was overwhelmed. I read through the first few posts of Matt Warren’s “tutorial” here: [http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx](http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx). I didn’t even understand half of what he was talking about, but I copied his code and started tinkering. I gave up a couple of times, thinking this would take way too long to implement. But, I always came back to wanting to use LINQ for my repositories. This meant I had to do some kind of LINQ-to-Ektron search translation and that meant I had do a LINQ query provider for it.
|
||||||
|
|
||||||
|
I actually got to the point where I had a working query provider that successfully performed searches through Ektron. It was pretty exciting. Somewhere along the way, however, I discovered Remotion’s re-linq library and I knew I needed to rewrite my entire library. re-linq does a great job of pre-parsing any LINQ expression into a more consistent format to translate into whatever you want to translate it into. You still have to do the actual work of translating the query into something useful, but the gazillions of tiny tasks of parsing the query, evaluating evaluatable parts, etc are all taken care of for you. In addition, it gives real structure to your query parsing code.
|
||||||
|
|
||||||
|
What I ended up with, I decided to put on GitHub as an open-source project so that other Ektron developers (which I do NOT claim to be!) could benefit from it. I’ve also created a NuGet package out of it to easily include in your own projects.
|
||||||
|
|
||||||
|
I need users and contributors to beef it up and test it out. So far, I’ve only had time to test it with the one project I developed it for (which is an Ektron 8.5 installation). So, my domain of testing has been pretty limited. I need it tested/expanded for 8.6 and for all sorts of different uses.
|
||||||
|
|
||||||
|
Check out the library here:
|
||||||
|
|
||||||
|
GitHub: [https://github.com/benjaminramey/GoodlyFere.Ektron.Linq](https://github.com/benjaminramey/GoodlyFere.Ektron.Linq)
|
||||||
|
NuGet: [http://nuget.org/packages/GoodlyFere.Ektron.Linq](http://nuget.org/packages/GoodlyFere.Ektron.Linq)
|
||||||
|
|
||||||
|
PS A note on the name “Goodly Fere”. Goodly Fere is the name given to Christ in a ballad by Ezra Pound. Read it here: [http://www.bartleby.com/265/295.html](http://www.bartleby.com/265/295.html). My faith in Christ defines everything I do and say (at least, it should–often I fall short of that ideal). Colossians 3:17 says to do everything in the name of the Lord Jesus. That’s quite a standard! After all, Paul started off that letter to the Colossians talking about how all of Creation was made by, through and for Christ. I think Creation is a pretty high standard of quality. I desire to reflect that standard of quality (as much as I can) in everything I do too–including writing code libraries.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Converting an Interface Expression to a Concrete Expression"
|
||||||
|
date: 2013-01-23
|
||||||
|
description: "Converting an Interface Expression to a Concrete Expression"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net]
|
||||||
|
---
|
||||||
|
I had a case recently where I needed to convert a LINQ expression of the type of an interface and I needed to convert it to be based on a concrete implementation of that interface. I came up with the following solution, using an ExpressionVisitor and a simple helper method.
|
||||||
|
|
||||||
|
First, the helper method in a static class.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
internal static Expression<Func<TConcrete, bool>> ConvertToConcreteExpression<TConcrete, TInterface>( Expression<Func<TInterface, bool>> interfaceExpression)
|
||||||
|
{
|
||||||
|
if (!typeof(TInterface).IsAssignableFrom(typeof(TConcrete)))
|
||||||
|
{
|
||||||
|
throw new Exception("TInterface must be assignable from TConcrete to convert an expression.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransformVisitor<TConcrete, TInterface>.Transform(interfaceExpression);
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Here is the TransformVisitor class.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
internal class TransformVisitor<TConcrete, TInterface> : ExpressionVisitor
|
||||||
|
{
|
||||||
|
private readonly ParameterExpression _param = Expression.Parameter(typeof(TConcrete), "param_0");
|
||||||
|
|
||||||
|
public static Expression<Func<TConcrete, bool>> Transform(Expression expression)
|
||||||
|
{
|
||||||
|
var visitor = new TransformVisitor<TConcrete, TInterface>();
|
||||||
|
var newLambda = (Expression<Func<TConcrete, bool>>)visitor.Visit(expression);
|
||||||
|
return newLambda;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitLambda<T>(Expression<T> node)
|
||||||
|
{
|
||||||
|
if (typeof(T).IsAssignableFrom(typeof(Func<TInterface, bool>)))
|
||||||
|
{
|
||||||
|
return Expression.Lambda<Func<TConcrete, bool>>(
|
||||||
|
Visit(node.Body),
|
||||||
|
_param
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.VisitLambda(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitMember(MemberExpression node)
|
||||||
|
{
|
||||||
|
if (node.Member.DeclaringType.IsAssignableFrom(typeof(TInterface)))
|
||||||
|
{
|
||||||
|
return Expression.MakeMemberAccess(
|
||||||
|
Visit(node.Expression),
|
||||||
|
typeof(TConcrete).GetProperty(node.Member.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.VisitMember(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitParameter(ParameterExpression node)
|
||||||
|
{
|
||||||
|
if (node.Type.IsAssignableFrom(typeof(TInterface)))
|
||||||
|
{
|
||||||
|
return _param;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.VisitParameter(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
45
src/posts/2013/03/15/ektron-paths.md
Normal file
45
src/posts/2013/03/15/ektron-paths.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Ektron Paths"
|
||||||
|
date: 2013-03-15
|
||||||
|
description: "Ektron Paths"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ektron]
|
||||||
|
---
|
||||||
|
For future reference, here is how you have to construct paths for finding Ektron content items, folders and taxonomy items.
|
||||||
|
|
||||||
|
## FolderManager and ContentManager
|
||||||
|
|
||||||
|
Use forward slashes (/)
|
||||||
|
Do NOT include an initial slash
|
||||||
|
DO include a trailing slash
|
||||||
|
Example: “folder1/folder2/otherfolder/”
|
||||||
|
Given this code:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
FolderManager fm = new FolderManager();
|
||||||
|
FolderCriteria folderCrit = new FolderCriteria();
|
||||||
|
folderCrit.AddFilter(
|
||||||
|
FolderProperty.FolderPath,
|
||||||
|
CriteriaFilterOperator.EqualTo,
|
||||||
|
folderPath);
|
||||||
|
|
||||||
|
FolderData folder = fm.GetList(folderCrit).FirstOrDefault();
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Then the “folderPath” variable needs to be in this format: “folderName1/folderName2/”. The same would apply for finding all the content in a folder via the contents’ path.
|
||||||
|
|
||||||
|
## TaxonomyManager
|
||||||
|
|
||||||
|
Use backslashes (\)
|
||||||
|
DO use an initial slash
|
||||||
|
Do NOT use a trailing slash
|
||||||
|
Example: “\taxonomy1\taxonomy2\othertaxonomy”
|
||||||
|
So, given this code:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
ITaxonomyManager _taxManager = ObjectFactory.GetTaxonomyManager();
|
||||||
|
TaxonomyData tax = _taxManager.GetItem(taxPath);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
The “taxPath” variable needs to be in this format: “\rootTaxonomyName\subTaxonomyName1\subTaxonomyName2″.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "TaxonomyManager WCF Error in Ektron 8.5 3-tier Setup"
|
||||||
|
date: 2013-03-15
|
||||||
|
description: "TaxonomyManager WCF Error in Ektron 8.5 3-tier Setup"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ektron]
|
||||||
|
---
|
||||||
|
## Problem
|
||||||
|
I’m currently working on a project to import data into an existing Ektron 8.5 SP3 installation. It is pulling existing product data from a database, transforming it into the smart form XML we need and then shoving that data into the Ektron installation. In addition to creating content items, the import tool also updates metadata and taxonomy fields.
|
||||||
|
|
||||||
|
The update the taxonomy fields, I use the TaxonomyManager to get the taxonomy by name, then set the taxonomy IDs on TaxonomyItemData objects. This import tool isn’t running inside of an Ektron installation–it’s a standalone tool–so it takes advantage of Ektron 8.5’s 3-tier setup. This means the TaxonomyManager calls are going over the wire via the WCF services.
|
||||||
|
|
||||||
|
The TaxonomyManager calls were throwing WCF errors, however, saying that the ITaxonomyManager interface was violating the WCF rules of not having any same-named methods. If you look, the ITaxonomyManager interface defines multiple same-named methods, including two versions of GetList.
|
||||||
|
|
||||||
|
This was pretty confusing to me because I couldn’t believe that Ektron would release a major version like 8.5, touting their 3-tier capabilities, without having tested one of the major content managers liket he TaxonomyManager.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Whatever the case may be, I started snooping around on the net for others having experienced this issue. As usual though (also a mystery), I couldn’t find any info on it. Why does there seem to be so little content on the net about Ektron??
|
||||||
|
|
||||||
|
I started snooping around with decompiling the Ektron.Cms.ObjectFactory DLL code where the ITaxonomyManager interface is defined. Everything looked good–it had the same-named methods, but was using the OperationContract attribute which lets you define the action name. The two GetList actions were defined with different names, so something wasn’t matching up with the error I was seeing in my import project. You can see that the two GetList methods have different Action values, making it OK for WCF.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
[OperationContract(Action="GetList")]
|
||||||
|
List<TaxonomyData> GetList(TaxonomyCriteria criteria);
|
||||||
|
|
||||||
|
[OperationContract(Action="GetListByCustomProperty", Name="GetListByCustomPropertyCriteria")]
|
||||||
|
List<TaxonomyData> GetList(TaxonomyCustomPropertyCriteria criteria);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
When I decompiled the Ektron.Cms.ObjectFactory DLL that I was using in my import project, I found the problem. You can see it here:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
[OperationContract(Action="GetList")]
|
||||||
|
List<TaxonomyData> GetList(TaxonomyCriteria criteria);
|
||||||
|
|
||||||
|
[OperationContract(Action="GetList")]
|
||||||
|
List<TaxonomyData> GetList(TaxonomyCustomPropertyCriteria criteria);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Notice how the Action is defined with the same name. I checked the DLL versions of my DLL and the DLL in the Ektron 8.5 SP3 site I was pushing content too. They were both the same: 8.5.0.356. So, somewhere along the line, Ektron fixed the issue without updating the AssemblyFileVersion to signify a bug fix/updated code. Once I copied the DLL from the SP3 site’s bin folder to my import project, it worked just fine.
|
||||||
|
|
||||||
|
So, if you have the same problem somewhere, look for an updated Ektron.Cms.ObjectFactory 8.5 DLL–probably in SP3.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Ektron Html Encodes Certain Content Title Characters"
|
||||||
|
date: 2013-03-20
|
||||||
|
description: "Ektron Html Encodes Certain Content Title Characters"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ektron]
|
||||||
|
---
|
||||||
|
I found out today, while trying to search for certain content items in Ektron, that Ektron HTML encodes certain characters in content titles–but does not, apparently, HTML encode the entire title.
|
||||||
|
|
||||||
|
Here is a list of characters I’ve found so far that Ektron encodes in the title:
|
||||||
|
|
||||||
|
- / (forward slash)
|
||||||
|
- ‘ (apostrophe – single quote)
|
||||||
|
- & (ampersand)
|
||||||
|
|
||||||
|
Here is what I know they do NOT encode in titles:
|
||||||
|
|
||||||
|
® (registered trademark symbol)
|
||||||
|
|
||||||
|
So, if you need to perform a search for Ektron content with a title like “Miner’s Hat”, then you need to encode it as “Miner's Hat” to find it.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "ASP.NET MVC 3.0 Redirect to Account/Login with Windows Authentication"
|
||||||
|
date: 2013-04-23
|
||||||
|
description: "ASP.NET MVC 3.0 Redirect to Account/Login with Windows Authentication"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,asp.net,mvc]
|
||||||
|
---
|
||||||
|
[ASP.NET MVC 3 Windows Authentication problem – redirects to Account/Login](http://www.benramey.com/2013/04/23/asp-net-mvc-3-0-redirect-to-accountlogin-with-windows-authentication/#)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Sitecore Rocks and SharePoint Package Interference"
|
||||||
|
date: 2013-05-08
|
||||||
|
description: "Sitecore Rocks and SharePoint Package Interference"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitecore]
|
||||||
|
---
|
||||||
|
I ran into an interesting problem today while working in Visual Studio 2010 on a SharePoint 2010 project.
|
||||||
|
|
||||||
|
You are supposed to be able to open the Package.package files to edit the Features that are included and their order, etc. However, when I double-clicked on the Package.package file to open it, I got a strange dialog box that said the document was already open, would I like to close it? If I said “yes”, another “Package Connection” dialog box would open with all the IIS sites I had setup listed. However, none of my SharePoint sites were listed and choosing any of the other sites just failed.
|
||||||
|
|
||||||
|
I have no idea why, honestly, but apparently Sitecore Rocks is causing this issue. I had the Visual Studio extension enabled (I had worked on a Sitecore project a few months ago). When I disabled it and restarted Visual Studio, everything worked as I expected when opening Package files.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint 2010 401 Unauthorized Error"
|
||||||
|
date: 2013-05-23
|
||||||
|
description: "SharePoint 2010 401 Unauthorized Error"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
|
||||||
|
There are many, many reasons why SharePoint will throw a 401 error. Most of them are hard to track down and have absolutely no bearing on reality unless you’re a genius. So, your only hope is to track down obscure registry updates (hacks), scour the internet for random things other people have found or just give up. :-)
|
||||||
|
|
||||||
|
I just about pulled my hair out yesterday trying to get to the bottom of a 401 Unauthorized error I kept getting for my anonymous access-enabled web application. I’m not sure why, but I never tried debugging my application to see where the error was actually being thrown. I assumed it was on the IIS level or some SharePoint level, so I never tried debugging.
|
||||||
|
|
||||||
|
So, lesson #1 when getting 401 in SharePoint: debug your application.
|
||||||
|
|
||||||
|
Lesson #2: apparently accessing the DefaultPage property of a PublishingWeb requires elevated privileges. Here was my code:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
private string GetDefaultPageUrl()
|
||||||
|
{
|
||||||
|
if (!PublishingWeb.IsPublishingWeb(SPContext.Current.Web))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishingWeb web = PublishingWeb.GetPublishingWeb(SPContext.Current.Web);
|
||||||
|
if (web == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.DefaultPage == null ? string.Empty : web.DefaultPage.Url;
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
That last line was throwing the 401. So, the solution was to wrap it in an SPSecurity.RunWithElevatedPrivileges call, like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
private string GetDefaultPageUrl()
|
||||||
|
{
|
||||||
|
string url = string.Empty;
|
||||||
|
SPSite currentSite = SPContext.Current.Site;
|
||||||
|
SPWeb currentWeb = SPContext.Current.Web;
|
||||||
|
|
||||||
|
SPSecurity.RunWithElevatedPrivileges(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
// reopen site & web, otherwise defaultpage will throw an error
|
||||||
|
using (SPSite site = new SPSite(currentSite.ID))
|
||||||
|
using (SPWeb web = site.OpenWeb(currentWeb.ID))
|
||||||
|
{
|
||||||
|
if (!PublishingWeb.IsPublishingWeb(web))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
|
||||||
|
if (web == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = pubWeb.DefaultPage == null ? string.Empty : pubWeb.DefaultPage.Url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Thanks to this post for the answer about re-opening the site and web inside the RunWithElevatedPrivileges code:
|
||||||
|
[http://henry-chong.com/2010/11/sharepoint-anonymous-access-and-publishing-web-default-page](http://henry-chong.com/2010/11/sharepoint-anonymous-access-and-publishing-web-default-page).
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint LINQ (SPMetal) in Anonymous User Context"
|
||||||
|
date: 2013-05-23
|
||||||
|
description: "SharePoint LINQ (SPMetal) in Anonymous User Context"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
Refer to the links below to fix this issue of SPMetal LINQ queries failing in an anonymous user context in SharePoint 2010.
|
||||||
|
|
||||||
|
[Access Denied with LINQ-to-SharePoint](http://blog.tylerholmes.com/2011/04/access-denied-with-linq-to-sharepoint.html)
|
||||||
|
|
||||||
|
[Making Linq to SharePoint work for Anonymous users](http://jcapka.blogspot.com/2010/05/making-linq-to-sharepoint-work-for.html)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint Unknown Error After Database Attach"
|
||||||
|
date: 2013-06-06
|
||||||
|
description: "SharePoint Unknown Error After Database Attach"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
There are many reasons you might get one of those blank screens with just the text “An unknown error has occurred” when working with SharePoint. Just search for it online and you’ll find many fixes.
|
||||||
|
|
||||||
|
I found another one last night. One of our system admins setup a SharePoint web application for me for development purposes. We attached a database from a backup of our client’s production content. But, I couldn’t get to the site. It gave me the unknown error screen.
|
||||||
|
|
||||||
|
I noticed in the ULS logs that the page request was immediately being redirected to the Access Denied page, but that wasn’t rendering either.
|
||||||
|
|
||||||
|
As it turns out, the system admin that setup the site was let go a couple of weeks ago and as a result his Windows domain account was deactivated. However, he was the lone Site Collection Administrator on the site. Once I removed him and added myself, the site came up as expected.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Error on /_vti_bin/owssvr.dll?cs=65001 When Creating or Editing a SharePoint List View"
|
||||||
|
date: 2013-06-13
|
||||||
|
description: "Error on /_vti_bin/owssvr.dll?cs=65001 When Creating or Editing a SharePoint List View"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
|
||||||
|
> UPDATE (6/14/2013):
|
||||||
|
> After some further issues I encountered (unrelated to the OWSSVR.dll error, but on this module) I found out that the problem is with accessing query string parameters (at least). However, accessing the HttpRequest object’s Url property, for example, doesn’t cause this error.
|
||||||
|
|
||||||
|
> So, I moved my code back to the BeginRequest event (to solve my other problem) and then I just did a simple HttpContext.Current.Request.Url.AbsoluteUri.Contains(“owssvr”) check to go on and check the query string parameters or not.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
I was recently very frustrated by the dreaded “Cannot complete this action” error on a SharePoint 2010 project when trying to edit or create a new view for any list. I looked around and found several causes that others had pinpointed, but nothing really related to the specific situation I was seeing. I wasn’t seeing this on a particular list or set of lists. I could create an out-of-the-box SharePoint Document Library and get this error when trying to edit or create a view.
|
||||||
|
|
||||||
|
What finally got me on the right track was this link here that a co-worker of mine found:
|
||||||
|
|
||||||
|
[MSDN forum thread](http://social.msdn.microsoft.com/Forums/en-US/sharepointdevelopmentprevious/thread/ea0b1380-480f-4b2e-afde-77ed06995bb0)
|
||||||
|
|
||||||
|
In a response to the question in that thread, it is mentioned that accessing the SPContext static object in an HttpModule causes this error when the /_vti_bin/owssvr.dll is accessed. Great! There was only one problem for me: my HttpModule was not accessing the SPContext object anywhere. Nevertheless, when I removed my HttpModules from the web.config, the list editing and creation worked. So, it was definitely something with my modules.
|
||||||
|
|
||||||
|
Still, the post mentioned above got me on the right track. By commenting out code a bit at a time, I narrowed the problem down to a check for the HttpContext.Current.Request object that I was performing. This is probably the same error that is caused by the SPContext object, since, as I understand it, the SPContext object just wraps the HttpContext object.
|
||||||
|
|
||||||
|
I had the following code in my HttpModule. Here is what Init method looked like:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public void Init(HttpApplication context)
|
||||||
|
{
|
||||||
|
context.BeginRequest += SetupOutputFilter;
|
||||||
|
context.PreSendRequestHeaders += WritePdfHeaders;
|
||||||
|
context.PreSendRequestContent += WritePdfToOutput;
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Commenting out the tie into the BeginRequest event fixed the issue, so it had to be something there. SetupOutputFilter looked like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
private void SetupOutputFilter(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!IsPdfRequest)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse response = HttpContext.Current.Response;
|
||||||
|
_pdfStream = new PdfMemoryStream(response.Filter);
|
||||||
|
response.Filter = _pdfStream;
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Commenting out the if expression also fixed the issue. My IsPdfRequest property looked like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
private static bool IsPdfRequest
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(HttpContext.Current.Request["as"])
|
||||||
|
&& HttpContext.Current.Request["as"] == "pdf";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
And there you have my access to the HttpContext object. I kind of needed this though, so I thought it might be a timing issue. Maybe accessing this on BeginRequest was the problem. I took a look at the ordering of the HttpModule events to find a good replacement. This StackOverflow thread helped me there:
|
||||||
|
|
||||||
|
[StackOverflow thread](http://stackoverflow.com/questions/441421/httpmodule-event-execution-order)
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
By switching my SetOutputFilter method from the BeginRequest event to the PostReleaseRequestState event, I was able to fix the issue.
|
||||||
|
|
||||||
|
So, the lesson here is this: if you need HttpModules for your SharePoint 2010 project, make sure they do not access the HttpContext object anywhere in the BeginRequest event.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Properly Setting Up SharePoint 2010 Search PDF Indexing"
|
||||||
|
date: 2013-06-26
|
||||||
|
description: "Properly Setting Up SharePoint 2010 Search PDF Indexing"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[Adobe PDF IFilter Indexing with SharePoint 2010](http://nickgrattan.wordpress.com/2010/06/14/adobe-pdf-ifilter-indexing-with-sharepoint%C2%A02010/)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Querying a Lookup Field with CAML"
|
||||||
|
date: 2013-07-15
|
||||||
|
description: "Querying a Lookup Field with CAML"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint,caml]
|
||||||
|
---
|
||||||
|
[Caml Query with Lookup field](http://forums.asp.net/t/1127580.aspx/1)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "SharePoint 2010 Page Layout Button Javascript Error"
|
||||||
|
date: 2013-09-25
|
||||||
|
description: "SharePoint 2010 Page Layout Button Javascript Error"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
I ran across an issue today in SharePoint 2010 where the Page Layout button would not open. It would throw the following error in Internet Explorer:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
SCRIPT5007: Unable to get property ‘nodeName’ of undefined or null reference
|
||||||
|
cui.js, line 2 character 6422
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
The following link gave me the answer.
|
||||||
|
|
||||||
|
[SharePoint 2010 – Quick Fix for Ribbon Page Layout switch JavaScript error](http://johnliu.net/blog/2010/12/22/sharepoint-2010-quick-fix-for-ribbon-page-layout-switch-java.html)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "How Not to Validate Email Addresses"
|
||||||
|
date: 2013-10-14
|
||||||
|
description: "How Not to Validate Email Addresses"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [email]
|
||||||
|
---
|
||||||
|
[How Not to Validate Email Addresses](http://mdswanson.com/blog/2013/10/14/how-not-to-validate-email-addresses.html)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Installing SharePoint 2010 Language Packs on Windows 7"
|
||||||
|
date: 2013-11-05
|
||||||
|
description: "Installing SharePoint 2010 Language Packs on Windows 7"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sharepoint]
|
||||||
|
---
|
||||||
|
[Installing Language Packs on Windows 7 for SharePoint Server 2010 RC](http://johnnyharbieh.wordpress.com/2010/04/08/installing-language-packs-on-windows-7-for-sharepoint-server-2010-rc/)
|
||||||
9
src/posts/2014/02/23/better-gruntfiles.md
Normal file
9
src/posts/2014/02/23/better-gruntfiles.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Better Gruntfiles"
|
||||||
|
date: 2014-02-23
|
||||||
|
description: "Better Gruntfiles"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [grunt]
|
||||||
|
---
|
||||||
|
[More maintainable Gruntfiles](http://www.thomasboyt.com/2013/09/01/maintainable-grunt.html)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "NServiceBus with EntityFramework Persisters"
|
||||||
|
date: 2014-03-07
|
||||||
|
description: "NServiceBus with EntityFramework Persisters"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,nservicebus]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update 1/13/2016
|
||||||
|
This code (much refactored and improved) is now available as a [NuGet package](http://www.nuget.org/packages/GoodlyFere.NServiceBus.EntityFramework).
|
||||||
|
|
||||||
|
Please see documentation and the code over at the [GitHub repository](https://github.com/benjaminramey/GoodlyFere.NServiceBus.EntityFramework).
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
NServiceBus is easily configurable to use NHibernate, RavenDB or in-memory persistence. RavenDB is built in and is the default. You can configure it to use in-memory persistence. You can install a NuGet package to use NHibernate which opens the door to many data stores.
|
||||||
|
|
||||||
|
So, for a recent project in which we were using NServiceBus, I decided to use NHibernate as the ORM for our simple domain model. I might just be dumb, but I ran into all sorts of problems with NHibernate and MS DTC when I used a remote SQL server. Try as I might, I just couldn't get NHibernate to work to persist my domain model even though it persisted the NServiceBus stuff just fine.
|
||||||
|
|
||||||
|
I decided to switch over to EntityFramework for my domain model. NHibernate and EntityFramework coexisted just fine, but I wanted to use the same ORM for both NServiceBus and my domain model just to make things nice and clean. The problem is, there isn't a prepackaged solution (none that I found at least) out there to use EntityFramework with NServiceBus. I had to implement my own persistence classes for NServiceBus. As it turns out, that wasn't too hard. Below is a description of what you will need to do to get EntityFramework working as NServiceBus' persistence layer. Much of it was modeled directly off of the NHibernate persistence classes found here: https://github.com/Particular/NServiceBus.NHibernate.
|
||||||
|
|
||||||
|
Below are all the classes and interfaces I used to implement these persisters. Note that I have EF abstracted away through an IDataContext interface which the persisters use.
|
||||||
|
|
||||||
|
- IPersistSagas
|
||||||
|
- ISubscriptionStorage
|
||||||
|
- IPersistTimeouts
|
||||||
|
|
||||||
|
Actually, this solution isn't tied specifically to EntityFramework. I have EF abstracted away through an IDataContext interface which the persisters use.
|
||||||
|
|
||||||
|
## IDataContext
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421173.js" type="text/javascript">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## BaseDataContext
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421291.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFDataContext
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421303.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## IRepository
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421272.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFRepository
|
||||||
|
Not every method is implemented because I didn't need them all on this project. Using TDD, I only implemented methods as I needed them.
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421323.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFDbContext
|
||||||
|
This is the project-specific EntityFramework DbContext.
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421353.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFSagaPersister
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421364.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFSubscriptionPersister
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421372.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## EFTimeoutPersister
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421383.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## TimeoutDataEntity
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421403.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## Subscription
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421461.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## SagaData
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421470.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## Criteria classes
|
||||||
|
<script src="https://gist.github.com/benjaminramey/9421428.js">
|
||||||
|
</script>
|
||||||
14
src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md
Normal file
14
src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "The Dumbest Code I Ever Wrote"
|
||||||
|
date: 2014-03-21
|
||||||
|
description: "The Dumbest Code I Ever Wrote"
|
||||||
|
categories: [programming]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
Here it is, folks: some of the dumbest code I’ve ever written and I just now noticed it.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
string.Concat("attachment; filename=\"", string.Concat(pagePdf.Name, ".pdf"), "\"");
|
||||||
|
{% endhighlight %}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "EPiServer 7 “The file ‘/link/GUID.aspx’ does not exist.” Error"
|
||||||
|
date: 2014-03-27
|
||||||
|
description: "EPiServer 7 “The file ‘/link/GUID.aspx’ does not exist.” Error"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [episerver]
|
||||||
|
---
|
||||||
|
|
||||||
|
I ran into the following error while working with an installation of EPiServer 7 this week. It took me a long time to finally figure out the solution.
|
||||||
|
|
||||||
|
First, here’s the error and stack trace:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
2014-03-27 12:12:43,396 [6] ERROR EPiServer.Global: 1.2.5 Unhandled exception in ASP.NET
|
||||||
|
System.Web.HttpException (0x80004005): The file ‘/link/cc299342a697494c8a4bc47717210bf0.aspx’ does not exist.
|
||||||
|
at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate)
|
||||||
|
at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate)
|
||||||
|
at System.Web.Compilation.BuildManager.GetVirtualPathObjectFactory(VirtualPath virtualPath, HttpContext context, Boolean allowCrossApp, Boolean throwIfNotFound)
|
||||||
|
at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)
|
||||||
|
at System.Web.Routing.PageRouteHandler.GetHttpHandler(RequestContext requestContext)
|
||||||
|
at System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context)
|
||||||
|
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
|
||||||
|
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
As it turns out, I did not have a TemplateDescriptor attribute on the page template that I was trying to access. Adding the following TemplateDescriptor attribute to the code-behind class for my page template saved me.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
[TemplateDescriptor(Path = "~/Templates/PageTemplates/Home.aspx")]
|
||||||
|
public partial class Home : TemplatePage<HomePage>
|
||||||
|
{% endhighlight %}
|
||||||
47
src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md
Normal file
47
src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "EPiServer 7: Cannot decrypt password"
|
||||||
|
date: 2014-04-17
|
||||||
|
description: "EPiServer 7: Cannot decrypt password"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [episerver]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
I’ve been dealing with an issue in EPiServer 7 recently. I got this error when trying to log into the admin back-end of the site.
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
Cannot decrypt password
|
||||||
|
|
||||||
|
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
|
||||||
|
|
||||||
|
Exception Details: System.InvalidOperationException: Cannot decrypt password
|
||||||
|
|
||||||
|
Source Error:
|
||||||
|
|
||||||
|
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
|
||||||
|
|
||||||
|
Stack Trace:
|
||||||
|
|
||||||
|
[InvalidOperationException: Cannot decrypt password]
|
||||||
|
EPiServer.Common.Security.HMACPasswordProvider.DecryptPassword(Byte[] ciphertext) +60
|
||||||
|
EPiServer.Common.Web.Authorization.Integrator.SynchronizeUser(MembershipUser membershipUser, String password, Boolean enableCreateNew) +1116
|
||||||
|
System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80
|
||||||
|
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +165
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## Scenario
|
||||||
|
We originally installed the Relate+ site for our EPiServer installation. Through various bad decisions, we didn’t keep the custom code and templates we wrote separate from the Relate+ stuff. So, we inevitably created a mess where we did not need a ton of the Relate+ templates, page types, block types, etc but we could not longer easily pull our custom stuff out from the tangled mess.
|
||||||
|
|
||||||
|
Well, we undertook the disentangling anyway (a good decision) and ended up with a much cleaner code base. This meant re-installing EPiServer for a clean (without Relate+) beginning. The database would remain the same to keep our content.
|
||||||
|
|
||||||
|
We ran into this error when we got the new, cleaned-up site installed and running and then tried to log into the administration portion of the site.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
At first, I found this post online: http://world.episerver.com/Modules/Forum/Pages/Thread.aspx?id=77776 which suggested changing over to the multiplexing membership and role providers and described the new hashing mechanism in EPiServer 7.
|
||||||
|
|
||||||
|
At first, this seemed to work. Yay! But, the joy didn’t last long. I soon got the error again even after using the multiplexing providers. I dug a little deeper into the issue.
|
||||||
|
|
||||||
|
What I found was (in retrospect) pretty obvious. When we did the new, clean installation of EPiServer, it also created a new web.config with new attributes on the machineKey element. These properties are used with forms authentication to encrypt and decrypt information. Well, we didn’t make sure to keep the same values between the old install and the new install. So, when it tried to decrypt login information created with the old descryptionKey using the new decryptionKey, it obviously didn’t work.
|
||||||
|
|
||||||
|
Luckily, our trusty Systems Admins always keep backups of stuff they replace. We just went into the old web.config file from the previous site, copied the entire <machineKey> element and overwrite it in the new web.config. After an app pool recycle, the login was working great.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Create a List Selector Field in a Widget Designer in Sitefinity 6.2"
|
||||||
|
date: 2014-05-21
|
||||||
|
description: "Create a List Selector Field in a Widget Designer in Sitefinity 6.2"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
|
||||||
|
> [9/25/14 update] See Hardy's comments below for necessary updates to my steps when using Sitefinity 7.1 and up. Thanks, Hardy!
|
||||||
|
|
||||||
|
## Scenario
|
||||||
|
|
||||||
|
I am designing a fairly simple widget in Sitefinity (version 6.2, but this probably applies to other versions as well) that will display a simple FAQ list. The FAQ list has two major parts: 1) a "table of contents" type ordered-list at the top that lists each question and links to the answer further down the page, and 2) the list of questions with the answers below them.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
I wanted to create a simple Sitefinity list that would contain the FAQ content (questions and answers). The default fields for a Sitefinity list are Title and Content which are really all I needed to collect a Question and Answer for a FAQ item.
|
||||||
|
|
||||||
|
So, my widget, after being placed on a page, would only need to allow a content author to simply select the FAQ list they had created earlier. The widget would then find the list, collect the items in the list and display them.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
My solution would involved a widget designer control and a customized FlatSelector field. Here's an overview with details below.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
1. Create a Sitefinity MVC widget with a single "List" field that will contain the list GUID
|
||||||
|
1. Create a Sitefinity widget designer for this MVC widget with a single FlatSelector field
|
||||||
|
1. Update the FlatSelector ItemType field
|
||||||
|
1. Update the FlatSelector ServiceURL field
|
||||||
|
1. Update the FlatSelector DataMembers
|
||||||
|
1. Update the widget designer Javascript to choose the list ID
|
||||||
|
|
||||||
|
|
||||||
|
### Details
|
||||||
|
For some reason, Sitefinity widget designer documentation is really hard for me to find. Maybe I'm the only one. Maybe Sitefinity needs to put some more dollars toward good documentation! Anyway, after some searching and some trial and error, I found this page giving me an idea of how I should accomplish what I was trying to do: [Sitefinity article][sf-article]
|
||||||
|
|
||||||
|
To summarize, it tells you how to create a widget designer with Sitefinity Thunder and then change the generated control to point to some of the generic content types (like Lists). However, it didn't have all the details I needed. I had to troubleshoot and that's why I'm writing this post.
|
||||||
|
|
||||||
|
#### Step 1: Create the Widget
|
||||||
|
Below are the details for how I setup my MVC widget. Translate the various tasks for the same effect if you prefer a WebForms widget.
|
||||||
|
|
||||||
|
Using Sitefinity Thunder, create your widget. Don't create the designer at this time. I like using MVC widgets but there's no reason this won't work with a WebForms widget. I called my widget "FAQList".
|
||||||
|
|
||||||
|
##### Step 1.1: Update controller properties
|
||||||
|
Remove whatever properties Thunder automatically adds on the MVC controller and add your own List property of type Guid.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
[Category("Widget Properties")]
|
||||||
|
public Guid List { get; set; }
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
##### Step 1.2: Update widget view model
|
||||||
|
I added a simple POCO to describe each FAQ item (containing a question and answer) and then added a list of these POCOs as the only property on my view model.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class FAQListModel
|
||||||
|
{
|
||||||
|
public List<FAQItem> Items { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FAQItem
|
||||||
|
{
|
||||||
|
public string Answer { get; set; }
|
||||||
|
public string Question { get; set; }
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
##### Step 1.3: Update Index action
|
||||||
|
Update the Index action to use the List property to access the content-author-chosen list, pull the items and stick them in the model.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public ActionResult Index()
|
||||||
|
{
|
||||||
|
var model = new FAQListModel();
|
||||||
|
|
||||||
|
if (List != Guid.Empty)
|
||||||
|
{
|
||||||
|
model.Items = App.WorkWith()
|
||||||
|
.List(List)
|
||||||
|
.ListItems()
|
||||||
|
.Get()
|
||||||
|
.Where(li => li.Status == ContentLifecycleStatus.Live)
|
||||||
|
.Select(li => new FAQItem { Question = li.Title, Answer = li.Content })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
model.Items = new List<FAQItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("Default", model);
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
#### Step 2: Create widget designer/public
|
||||||
|
Once your widget is created, you can create your widget designer. Using Thunder, create a widget designer for an existing widget. Name your widget designer. I use the name of the widget and append "Designer".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click "Next" on the next screen. Choose your FAQItemController from the list on the next screen.
|
||||||
|
|
||||||
|
##### Step 2.1: Choose widget designer fields
|
||||||
|
Next, choose the List property from your controller and set it up as follows. Especially note two things. First, choose the DynamicContentSelector. Second, update the "Select the content type for the selector" to 'Telerik.Sitefinity.Lists.Model.List'. Click on "Add".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### Step 2.2: Update generated widget designer files
|
||||||
|
In the generated .ascx file, update the 'ServiceUrl' property on the FlatSelector control to '/Sitefinity/Services/Lists/ListService.svc/?managerType=&providerName=&itemType=Telerik.Sitefinity.Lists.Model.List&provider=&sortExpression=LastModified%20DESC&skip=0&take=50'.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the generated .cs file, remove line 135 that sets the ConstantFilter property to 'Visible=true'.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Finally, in the generated .js file, change the property that is used as the value of the selected list from 'OriginalContentId' to 'Id'. This code can be found in the '_ListDoneSelecting' method.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
At this point, you should be done! Build your project, plop your new widget on a page and test it out! Here's what mine looks like.
|
||||||
|
|
||||||
|
The edit dialog:
|
||||||
|

|
||||||
|
|
||||||
|
The list select screen:
|
||||||
|

|
||||||
|
|
||||||
|
The selected item screen:
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
[sf-article]: http://www.sitefinity.com/documentation/documentationarticles/change-your-dynamic-content-selector-to-choose-from-generic-content
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Sitefinity Thunder Boolean Widget Designer Field Auto-Generation is Flawed"
|
||||||
|
date: 2014-05-23
|
||||||
|
description: "Sitefinity Thunder Boolean Widget Designer Field Auto-Generation is Flawed"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [sitefinity]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
When you use Sitefinity Thunder (a great help, by the way) to generate a Widget Designer for an existing widget that has a boolean property, you get this javascript in the auto-generated javascript file (let’s say my boolean field is named HasSubMenu):
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
/* RefreshUI HasSubMenu */
|
||||||
|
jQuery(this.get_hasSubMenu()).attr("checked", controlData.HasSubMenu);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This code is in the refreshUI method. It’s supposed to mark a checkbox as checked if HasSubMenu is true and not check it if it’s false. The problem is, for some reason, HasSubMenu is a string in Javascript instead of a real boolean. So, HasSubMenu will be either “true” or “false”–both of which are true values in Javascript. That is, because “true” and “false” are both non-empty strings, they evaluate to true when used as an expression.
|
||||||
|
|
||||||
|
This means, no matter what you do with the checkbox, the next time you click “Edit” for this widget, the checkbox will be checked and your data is effectively corrupted.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
The solution is pretty simple. Instead of using controlData.HasSubMenu for the expression by itself, update your Javascript so that you check if the string is “true” or not: controlData.HasSubMenu === "true". The line in the refreshUI method should now look like this:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
/* RefreshUI HasSubMenu */
|
||||||
|
jQuery(this.get_hasSubMenu()).attr("checked", controlData.HasSubMenu === "true");
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Now your checkbox will keep it’s value as you would expect.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "IE Date Parsing Doesn’t Work Like Chrome"
|
||||||
|
date: 2014-06-05
|
||||||
|
description: "IE Date Parsing Doesn’t Work Like Chrome"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [ie,chrome]
|
||||||
|
---
|
||||||
|
|
||||||
|
I noticed a goofy issue today while working with a jQuery countdown plugin. This plugin allows you to set an “until” date so that you counter counts down to zero at a certain date and time.
|
||||||
|
|
||||||
|
I was setting the date like this:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
element.countdown({
|
||||||
|
until: new Date(’10/13/14 00:00:00′)
|
||||||
|
});
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
In Chrome, this worked just fine. The string “10/13/14 00:00:00″ parsed to a Date object for midnight on October 13th, 2014.
|
||||||
|
|
||||||
|
In IE, however (versions10, 9 and 8 at least) it parsed as October 13th, 1914. Why? Who knows. Updating my string to “10/13/2014 00:00:00″ fixed it. My final javascript looked like this:
|
||||||
|
|
||||||
|
{% highlight javascript %}
|
||||||
|
element.countdown({
|
||||||
|
until: new Date(’10/13/2014 00:00:00′)
|
||||||
|
});
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
So, be aware of this oddity! It caused my whole countdown widget to screw up because the current time was already past the “until” date.#
|
||||||
10
src/posts/2014/09/23/apply-git-patches-to-svn-repository.md
Normal file
10
src/posts/2014/09/23/apply-git-patches-to-svn-repository.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Apply Git Patches to SVN Repository"
|
||||||
|
date: 2014-09-23
|
||||||
|
description: "Apply Git Patches to SVN Repository"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [git]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Creating Subversion patches with Git](http://codeprairie.net/blogs/chrisortman/archive/2008/01/14/creating-subversion-patches-with-git.aspx)
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Active Directory Authentication in ASP.NET MVC 5 with Forms Authentication and Group-Based Authorization"
|
||||||
|
date: 2014-10-20 9:03:00
|
||||||
|
description: "How to integrate MVC authorization attributes with Active Directory."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [mvc,asp.net,active-directory]
|
||||||
|
---
|
||||||
|
|
||||||
|
I know that blog post title is sure a mouth-full, but it describes the whole problem I was trying to solve in a recent project.
|
||||||
|
|
||||||
|
## The Project
|
||||||
|
|
||||||
|
Let me outline the project briefly. We were building a report dashboard-type site that will live inside the client's network. The dashboard gives an overview of various, very important information that relates to how the company is performing on a hourly basis. So, the dashboard is only available to a certain group of directors.
|
||||||
|
|
||||||
|
To limit the solution to the these directors, authentication and authorization would go through their existing Active Directory setup by putting the authorized users in a special AD group.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Getting authentication to work was a snap. Microsoft provides the System.Web.Security.ActiveDirectoryMembershipProvider
|
||||||
|
class to use as your membership provider. Putting an `[Authorize]` attribute on my action methods or entire controllers was all I needed to get it working (besides, of course, the system.web/authentication web.config updates and a controller to show my login form and handle the submit credentials).
|
||||||
|
|
||||||
|
Here's my relevant web.config setup:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<connectionStrings>
|
||||||
|
<add name="ADConnectionString" connectionString="<ldap connection string here>" />
|
||||||
|
</connectionStrings>
|
||||||
|
…
|
||||||
|
<authentication mode="Forms">
|
||||||
|
<forms name=".AuthCookie" loginUrl="~/login"/>
|
||||||
|
</authentication>
|
||||||
|
<membership defaultProvider="ADMembershipProvider">
|
||||||
|
<providers>
|
||||||
|
<clear/>
|
||||||
|
<add name="ADMembershipProvider"
|
||||||
|
type="System.Web.Security.ActiveDirectoryMembershipProvider"
|
||||||
|
connectionStringName="ADConnectionString"
|
||||||
|
attributeMapUsername="sAMAccountName"/>
|
||||||
|
</providers>
|
||||||
|
</membership>
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
The tough part came when I wanted to limit access to users in that AD group. Microsoft doesn't provide a RoleProvider along with its ActiveDirectoryMembershipProvider. So, what to do?
|
||||||
|
|
||||||
|
I tried several methods I found online. Most of them were based on creating my own custom RoleProvider and querying AD to iterate through the user's groups (treating them like roles) and seeing if one of them matched my AD group I was looking for. However, I could never get it to work. Each code example I found eventually gave me this AD error when I iterated through the current user's AD groups:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
The specified directory service attribute or value does not exist.
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## The Solution
|
||||||
|
|
||||||
|
Eventually, I found a solution online that worked. Instead of setting up a custom RoleProvider, all it involved was creating a custom AuthorizeAttribute for your MVC controllers (or action methods) that checked the user's .IsMemberOf method to see if the member belonged the sought after group (or groups). I don't know why this method does not cause the same AD error as describe above, but I'm glad it doesn't! All I can assume is that it queries AD in a more friendly way.
|
||||||
|
|
||||||
|
Here is my custom AuthorizeAttribute:
|
||||||
|
|
||||||
|
{% highlight c# %}
|
||||||
|
public class AuthorizeADAttribute : AuthorizeAttribute
|
||||||
|
{
|
||||||
|
private bool _authenticated;
|
||||||
|
private bool _authorized;
|
||||||
|
|
||||||
|
public string Groups { get; set; }
|
||||||
|
|
||||||
|
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
|
||||||
|
{
|
||||||
|
base.HandleUnauthorizedRequest(filterContext);
|
||||||
|
|
||||||
|
if (_authenticated && !_authorized)
|
||||||
|
{
|
||||||
|
filterContext.Result = new RedirectResult("/error/notauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool AuthorizeCore(HttpContextBase httpContext)
|
||||||
|
{
|
||||||
|
_authenticated = base.AuthorizeCore(httpContext);
|
||||||
|
|
||||||
|
if (_authenticated)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Groups))
|
||||||
|
{
|
||||||
|
_authorized = true;
|
||||||
|
return _authorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = Groups.Split(',');
|
||||||
|
string username = httpContext.User.Identity.Name;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_authorized = LDAPHelper.UserIsMemberOfGroups(username, groups);
|
||||||
|
return _authorized;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.Log().Error(() => "Error attempting to authorize user", ex);
|
||||||
|
_authorized = false;
|
||||||
|
return _authorized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_authorized = false;
|
||||||
|
return _authorized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Notice that I also included a little code to distinguish between the user not being authenticated (which the call to base.AuthorizeCore takes care of) and not being authorized. Without the code in HandleUnauthorizedRequest, if the user successfully logs in but is not in the AD group, he just sees the log in screen again which doesn't communicate the problem very well.
|
||||||
|
|
||||||
|
The this.Log() code uses a Nuget package called this.Log. The LDAPHelper class is something I wrote. The code is below:
|
||||||
|
|
||||||
|
{% highlight c# %}
|
||||||
|
public static class LDAPHelper
|
||||||
|
{
|
||||||
|
public static string GetLDAPContainer()
|
||||||
|
{
|
||||||
|
Uri ldapUri;
|
||||||
|
ParseLDAPConnectionString(out ldapUri);
|
||||||
|
|
||||||
|
return HttpUtility.UrlDecode(ldapUri.PathAndQuery.TrimStart('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetLDAPHost()
|
||||||
|
{
|
||||||
|
Uri ldapUri;
|
||||||
|
ParseLDAPConnectionString(out ldapUri);
|
||||||
|
|
||||||
|
return ldapUri.Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ParseLDAPConnectionString(out Uri ldapUri)
|
||||||
|
{
|
||||||
|
string connString = ConfigurationManager.ConnectionStrings["ADConnectionString"].ConnectionString;
|
||||||
|
|
||||||
|
return Uri.TryCreate(connString, UriKind.Absolute, out ldapUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool UserIsMemberOfGroups(string username, string[] groups)
|
||||||
|
{
|
||||||
|
/* Return true immediately if the authorization is not
|
||||||
|
locked down to any particular AD group */
|
||||||
|
if (groups == null || groups.Length == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the user is in the given AD group (if any)
|
||||||
|
using (var context = BuildPrincipalContext())
|
||||||
|
{
|
||||||
|
var userPrincipal = UserPrincipal.FindByIdentity(context,
|
||||||
|
IdentityType.SamAccountName,
|
||||||
|
username);
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
if (userPrincipal.IsMemberOf(context, IdentityType.Name, group))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrincipalContext BuildPrincipalContext()
|
||||||
|
{
|
||||||
|
string container = LDAPHelper.GetLDAPContainer();
|
||||||
|
return new PrincipalContext(ContextType.Domain, null, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
My code is mostly based on example code I found on a very helpful [StackOverflow post][so-post].
|
||||||
|
|
||||||
|
To use this code, all you have to do is use your custom AuthorizeAttribute instead of the built-in one. Something like this:
|
||||||
|
|
||||||
|
{% highlight c# %}
|
||||||
|
[AuthorizeAD(Groups="Some AD group name")]
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
…
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
[so-post]: http://stackoverflow.com/questions/4342271/asp-net-mvc-forms-authorization-with-active-directory-groups/4383502#4383502
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Permutations with Iterators in C#"
|
||||||
|
date: 2015-09-03 12:27:00
|
||||||
|
description: "Link to great code for generator permutations of objects with iterators"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net]
|
||||||
|
---
|
||||||
|
|
||||||
|
I recently ran across a need to generate all permutations of an array of objects
|
||||||
|
in some unit testing I was doing.
|
||||||
|
|
||||||
|
The specific situation was testing that, no matter what the order, when certain messages
|
||||||
|
were picked up by an [NServiceBus][nsbus] [Saga][sagas], that a certain state was consistent after all
|
||||||
|
messages were received. Since I explicitly wanted to test the state no matter what order
|
||||||
|
the messages arrived in, I needed all permutations of those messages so I could send
|
||||||
|
them to the Saga in each order and test the resulting state.
|
||||||
|
|
||||||
|
I found this super article on the subject that gave all the code I needed: [Generating Permutations with C# Iterators][article].
|
||||||
|
|
||||||
|
Thanks IanG on Tap!
|
||||||
|
|
||||||
|
[nsbus]:http://particular.net/
|
||||||
|
[article]:http://www.interact-sw.co.uk/iangblog/2004/09/16/permuterate
|
||||||
|
[sagas]:particular.net/articles/sagas-in-nservicebus
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Working with x.509 Certificates in .NET"
|
||||||
|
date: 2015-12-02 16:35:00
|
||||||
|
description: "Link to helpful article on working with certificates in Windows and .NET"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,certificates]
|
||||||
|
---
|
||||||
|
|
||||||
|
I found a lot of very helpful details about certificate storage and usage in Windows and
|
||||||
|
.NET in this article.
|
||||||
|
|
||||||
|
Particularly, the explanation of where certificates and private keys can be stored for UserStore
|
||||||
|
certificates was very helpful in debugging some issues I'm currently having on a production server.
|
||||||
|
|
||||||
|
[Paul Stovell's blog post](http://paulstovell.com/blog/x509certificate2)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Adding a RabbitMQ Configuration File When Running as a Windows Service"
|
||||||
|
date: 2016-01-04 15:26:00
|
||||||
|
description: "Instructions to add a RabbitMQ configuration file after having installed the Windows Service already."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [rabbitmq]
|
||||||
|
---
|
||||||
|
## A little context
|
||||||
|
I was having a hard time adding a rabbitmq.config file today. I added it in the right location, restarted
|
||||||
|
the RabbitMQ Windows service and the logs still showed that the configuration file was not found.
|
||||||
|
|
||||||
|
If you're experiencing the same problem, you'll see a similar error message in the RabbitMQ log file after
|
||||||
|
you restart the Windows service.
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
=INFO REPORT==== <date here>
|
||||||
|
node : rabbit@<server>
|
||||||
|
home dir : C:\Windows
|
||||||
|
config file(s) : c:/path/to/config/rabbitmq.config (not found)
|
||||||
|
...some other stuff...
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This will happen if you just install your RabbitMQ server with the normal, default installation process
|
||||||
|
and then try to add a configuration file later. By default, RabbitMQ doesn't install configuration file and just
|
||||||
|
uses all it's defined defaults.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
I read the [documentation](http://www.rabbitmq.com/configure.html) a little more closely
|
||||||
|
and finally came upon this line: "Windows service users will need to re-install the service after adding
|
||||||
|
or removing a configuration file."
|
||||||
|
|
||||||
|
Oops. Guess I should have read the documentation more closely the first time around! The easiest way to
|
||||||
|
do this is as follows (start a command prompt as administrator):
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
> cd "C:\Program Files (x86)\RabbitMQ Server\rabbitmq_server-3.6.0\sbin"
|
||||||
|
> .\rabbitmq-service.bat remove
|
||||||
|
> .\rabbitmq-service.bat install
|
||||||
|
> .\rabbitmq-service.bat start
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Of course, The path you cd into will depend on the version of RabbitMQ you have installed.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "You Can't Subscribe to IEvent with NServiceBus"
|
||||||
|
date: 2016-02-25 15:56:00
|
||||||
|
description: "The problems with trying to subscribe to IEvent with NServiceBus"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,nservicebus]
|
||||||
|
---
|
||||||
|
|
||||||
|
The reason you might want to subscribe to `IEvent` is pretty straightforward. I
|
||||||
|
wanted to do it for the simple reason of wanting to be able to choose (based
|
||||||
|
on configuration) whether I wanted to notify anyone of any event that
|
||||||
|
could be published on my bus.
|
||||||
|
|
||||||
|
In other words, whenever an event of any type was published, I wanted one particular
|
||||||
|
endpoint to subscribe to that event and then decide whether it should do
|
||||||
|
anything with it.
|
||||||
|
|
||||||
|
I thought the code to do that should be pretty simple. Since all published
|
||||||
|
events *should* implement the `IEvent` interface and NServiceBus supports
|
||||||
|
polymorphism in its message handling, creating a message handler that
|
||||||
|
implemented `IHandleMessages<IEvent>` should be all I should have needed!
|
||||||
|
|
||||||
|
I was testing this with only partial success and so I thought it was working.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class EventHandler : IHandleMessages<IEvent>
|
||||||
|
{
|
||||||
|
public void Handle(IEvent message)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## My Partial Success
|
||||||
|
The reason I had partial success was because I was also *explicitly* handling
|
||||||
|
other events in the same endpoint on a special saga I had setup. So, when
|
||||||
|
the endpoint started up, it would subscribe to these events. At first, I only
|
||||||
|
wanted to send notifications for these events anyway, so it all seemed to be
|
||||||
|
working. My special `IEvent` would work because the `Saga` subscribed this same
|
||||||
|
endpoint to those few events explicitly.
|
||||||
|
|
||||||
|
## My Full Failure
|
||||||
|
I realized I had only partially succeeded when I started wanting to send notifications
|
||||||
|
for other events that were not in that `Saga`. I dug into the issue for a few
|
||||||
|
hours and finally began to wonder if NServiceBus (or maybe the RabbitMQ transport)
|
||||||
|
was explicitly ignoring `IEvent` when it setup subscriptions.
|
||||||
|
|
||||||
|
Sure enough, it does.
|
||||||
|
|
||||||
|
To build a list of types to subscribe to, NServiceBus uses the `Conventions`
|
||||||
|
class in the `NServiceBus.Core` namespace. One of the checks that code does
|
||||||
|
is to filter the list of potential types with the `IsEventType` method. This
|
||||||
|
method checks if the `Type` is in a Particular (NServiceBus) DLL. See the
|
||||||
|
code here: [IsEventType](https://github.com/Particular/NServiceBus/blob/e4bc405509e3b9c3fc91e21a56333bb40ac54a60/src/NServiceBus.Core/Conventions.cs#L154)
|
||||||
|
|
||||||
|
## My Solution
|
||||||
|
The solution is simple enough. Instead of listening to `IEvent`, listen
|
||||||
|
to a custom interface that you implement on all of your events. This is probably
|
||||||
|
more close to what you are trying to do anyway--listen to all events that your
|
||||||
|
system produces.
|
||||||
|
|
||||||
|
So, I simply created a marker interface called `ICustomEvent`.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public interface ICustomEvent : IEvent { }
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Then, on all of my bus event classes, instead of implementing `IEvent` directly,
|
||||||
|
I implemented `ICustomEvent`.
|
||||||
|
|
||||||
|
My handler then looked like this.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class EventHandler : IHandleMessages<ICustomEvent>
|
||||||
|
{
|
||||||
|
public void Handle(ICustomEvent message)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Now, the right subscriptions are all set up and my single handler gets
|
||||||
|
every single event published by my entire bus.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Using NuGetPackager 0.5.5 with GitVersionTask 3.4.1"
|
||||||
|
date: 2016-03-03 15:04:00
|
||||||
|
description: "A quick fix to use NuGetPackager 0.5.5 with GitVersionTask 3.4.1"
|
||||||
|
categories: [programming]
|
||||||
|
tags: [nuget,git]
|
||||||
|
---
|
||||||
|
I'm using both [NuGetPackager](https://www.nuget.org/packages/NuGetPackager/)
|
||||||
|
with [GitVersionTask](https://www.nuget.org/packages/GitVersionTask/) on
|
||||||
|
my project that provides EntityFramework persisters for NServiceBus:
|
||||||
|
[GoodlyFere.NServiceBus.EntityFramework](https://github.com/benjaminramey/GoodlyFere.NServiceBus.EntityFramework).
|
||||||
|
|
||||||
|
NuGetPackager seems to work seamlessly with GitVersionTask 2.0.0. However,
|
||||||
|
GitVersionTask is up to version 3.4.1 and I just hate sitting at a version that
|
||||||
|
far behind!
|
||||||
|
|
||||||
|
So, I updated my NuGet package to the latest version of GitVersionTask. That's
|
||||||
|
3.4.1 right now. Too bad, so sad! That caused this error:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
The "CreatePackages" task was not given a value for the required parameter "Version". 1
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
The problem turns out that NuGetPackager 0.5.5 is looking for a MSBuild property
|
||||||
|
called "GfvNuGetVersion". See the code in the .targets file [here](https://github.com/Particular/NuGetPackager/blob/0.5.6/src/NuGetPackager/NuGetPackager.targets#L5).
|
||||||
|
|
||||||
|
GitVersionTask 3.4.1 (somewhere in a release since 2.0.0) changed that property
|
||||||
|
name to "GitVersion_NuGetVersion". See the code [here](https://github.com/GitTools/GitVersion/blob/master/src/GitVersionTask/NugetAssets/GitVersionTask.targets#L68).
|
||||||
|
|
||||||
|
## The Solution
|
||||||
|
So, how to fix this? NuGetPackager will probably be updated some day, but for
|
||||||
|
now you can just translate the property in your .csproj file before the
|
||||||
|
CreatePackage task is called in NuGetPackager.
|
||||||
|
|
||||||
|
Open up your .csproj file directly in some text editor like Notepad++. Find
|
||||||
|
these two lines at the bottom of the file:
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<Import Project="..\packages\GitVersionTask.3.4.1\Build\dotnet\GitVersionTask.targets"
|
||||||
|
Condition="Exists('..\packages\GitVersionTask.3.4.1\Build\dotnet\GitVersionTask.targets')" />
|
||||||
|
<Import Project="..\packages\NuGetPackager.0.5.5\build\NuGetPackager.targets"
|
||||||
|
Condition="Exists('..\packages\NuGetPackager.0.5.5\build\NuGetPackager.targets')" />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Before those two lines, add a target and a property group. The target will
|
||||||
|
create a task that translates the new property to the new property. The
|
||||||
|
property group wil define a BuildDependsOn to call that target before the
|
||||||
|
CreatePackages target is called.
|
||||||
|
|
||||||
|
{% highlight xml %}
|
||||||
|
<Target Name="TranslateNugetVersion"
|
||||||
|
Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<CreateProperty
|
||||||
|
Value="$(GitVersion_NuGetVersion)">
|
||||||
|
<Output
|
||||||
|
TaskParameter="Value"
|
||||||
|
PropertyName="GfvNuGetVersion" />
|
||||||
|
</CreateProperty>
|
||||||
|
</Target>
|
||||||
|
<PropertyGroup>
|
||||||
|
<BuildDependsOn>
|
||||||
|
$(BuildDependsOn);
|
||||||
|
TranslateNugetVersion
|
||||||
|
</BuildDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="..\packages\GitVersionTask.3.4.1\Build\dotnet\GitVersionTask.targets"
|
||||||
|
Condition="Exists('..\packages\GitVersionTask.3.4.1\Build\dotnet\GitVersionTask.targets')" />
|
||||||
|
<Import Project="..\packages\NuGetPackager.0.5.5\build\NuGetPackager.targets"
|
||||||
|
Condition="Exists('..\packages\NuGetPackager.0.5.5\build\NuGetPackager.targets')" />
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Now, when you build your project, everything should work great!
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "OWIN Integration Testing with OAuth Bearer Tokens"
|
||||||
|
date: 2016-04-11 13:34:00
|
||||||
|
description: "How to generate an OAuth bearer token to use with OWIN integration tests."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [owin,.net]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Situation
|
||||||
|
The situation is pretty simple. We have a project written with Web API 2.2
|
||||||
|
that we have running with OWIN. The API requires user authorization. To
|
||||||
|
accomplish that, we use the OWIN OAuth libraries.
|
||||||
|
|
||||||
|
Upon until recently, the OAuth authorization server (the thing that takes in
|
||||||
|
the user credentials and issues the token) and the API (the resource server) were
|
||||||
|
hosted in the same site and web project.
|
||||||
|
|
||||||
|
Integration testing was pretty easy at this point. For the authorized calls,
|
||||||
|
we simply wrote helper methods that logged a test user in through the authorization
|
||||||
|
server (hosted along with the API in the OWIN test server), got a bearer token
|
||||||
|
and then made the integration test calls.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
When we recently decided to split the authorization server and resource server
|
||||||
|
code up into two separate code bases (so that they could be hosted separately)
|
||||||
|
we ran into issues with the authorization and API calls no longer being possible within a
|
||||||
|
single integration test. The code to generate the bearer token was now no
|
||||||
|
longer accessible in the test server for the API integration tests because the authorization
|
||||||
|
server was now designed to be hosted separately. We'd have
|
||||||
|
to somehow generate the bearer token without making a call with the OWIN test
|
||||||
|
server.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
The solution is to generate a valid bearer token in your test code and send it
|
||||||
|
along with your test API call as you would have previous. How to generate that
|
||||||
|
valid bearer token though?? It turns out to be easier than I thought.
|
||||||
|
|
||||||
|
How you use OWIN's test server is beyond the scope of this blog post, but
|
||||||
|
eventually you'll have some code resembling this:
|
||||||
|
|
||||||
|
var server = TestServer.Create<Startup>();
|
||||||
|
|
||||||
|
You'll have to use a overload of the `Create()` method to capture a reference
|
||||||
|
to the server's `IDataProtector`. This interface defines what OWIN OAuth will
|
||||||
|
use to encrypt a `ClaimsIdentity` into a valid bearer token.
|
||||||
|
|
||||||
|
Update your server creation code like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
var dataProtector;
|
||||||
|
var server = TestServer.Create(app =>
|
||||||
|
{
|
||||||
|
var s = new Startup();
|
||||||
|
s.Configuration(app);
|
||||||
|
|
||||||
|
dataProtector = app.CreateDataProtector(
|
||||||
|
typeof(OAuthBearerAuthenticationMiddleware).Namespace,
|
||||||
|
"Access_Token", "v1");
|
||||||
|
});
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
You have to get the `dataProtector` this way because it's the same way that
|
||||||
|
the OWIN OAuth libraries get it before trying to decrypt the bearer token.
|
||||||
|
See the source for the OWIN OAuth code here:
|
||||||
|
[OAuthBearerAuthenticationMiddleware.cs](http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs)
|
||||||
|
|
||||||
|
Once you've "captured" the `dataProtector`, in your integration test (or in some
|
||||||
|
helper code, probably) you can generate your `ClaimsIdentity` and then your
|
||||||
|
bearer token with the `dataProtector`.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
// inside an integration test
|
||||||
|
using (server)
|
||||||
|
{
|
||||||
|
// create your valid identity any way you need to here
|
||||||
|
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity("username"));
|
||||||
|
|
||||||
|
// these classes are defined in the OWIN OAuth libraries.
|
||||||
|
var properties = new AuthenticationProperties();
|
||||||
|
var ticket = new AuthenticationTicket(identity, properties);
|
||||||
|
var format = new TicketDataFormat(dataProtector);
|
||||||
|
|
||||||
|
string bearerToken = format.Protect(ticket);
|
||||||
|
|
||||||
|
// ... call your API through the test server with the bearer token...
|
||||||
|
var response = server.CreateRequest("/api/someendpoint")
|
||||||
|
.AddHeader("Authorization", "Bearer " + bearerToken)
|
||||||
|
.GetAsync()
|
||||||
|
.Result;
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Castle.Windsor TypedFactoryFacility and Unexpected Property Injection Behavior"
|
||||||
|
date: 2016-08-24 12:37:00
|
||||||
|
description: "Interesting (and unexpected) behavior of Windsor's property injection when using TypedFactoryFacility."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [.net,castle-windsor]
|
||||||
|
---
|
||||||
|
|
||||||
|
I finally got to the bottom of an issue today that came down to the way Castle.Windsor
|
||||||
|
does property injection (injecting dependencies into public properties) when
|
||||||
|
you are also using their TypedFactoryFacility. It was a tough one to track down, so
|
||||||
|
I thought I'd put the solution out there for everyone else's benefit.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
Here's what the problem looked like.
|
||||||
|
|
||||||
|
### The Error Message
|
||||||
|
First things first, this is what the symptom looked like:
|
||||||
|
|
||||||
|
{% highlight text %}
|
||||||
|
[ComponentNotFoundException: No component for supporting the service System.Threading.Tasks.Task was found]
|
||||||
|
Castle.MicroKernel.DefaultKernel.Castle.MicroKernel.IKernelInternal.Resolve(Type service, IDictionary arguments, IReleasePolicy policy) +120
|
||||||
|
Castle.Facilities.TypedFactory.Internal.TypedFactoryInterceptor.Resolve(IInvocation invocation) +147
|
||||||
|
Castle.Facilities.TypedFactory.Internal.TypedFactoryInterceptor.Intercept(IInvocation invocation) +123
|
||||||
|
Castle.DynamicProxy.AbstractInvocation.Proceed() +448
|
||||||
|
Castle.Proxies.Func`2Proxy.Invoke(OAuthMatchEndpointContext arg) +168
|
||||||
|
|
||||||
|
...
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
This is actually a pretty standard error when you're using Castle.Windsor. It usually
|
||||||
|
means Windsor tried to resolve some dependency but discovered that it was missing a
|
||||||
|
registration. Translation: you forgot to register a component or type in your Windsor
|
||||||
|
setup. Usually it's an easy fix; just register your dependency properly.
|
||||||
|
|
||||||
|
In this case, however, as you can see, the missing registration was of type
|
||||||
|
System.Threading.Tasks.Task. I didn't have any dependencies on the `Task` class and so
|
||||||
|
I didn't want to "fix" any Windsor setup to register it. This wasn't making sense.
|
||||||
|
|
||||||
|
### Step One: TypedFactoryInterceptor?
|
||||||
|
The first oddity I noticed was that this error was happening somewhere in the flow
|
||||||
|
of the TypedFactoryInterceptor. I could tell where the error was coming from in my
|
||||||
|
code (from the rest of the stack trace that I didn't include above). The problem
|
||||||
|
was that it had nothing to do with any TypedFactoryFacility.
|
||||||
|
|
||||||
|
So, I inspected my registrations a little closer. I basically had a setup like this.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class ChildClass : ParentClass
|
||||||
|
{
|
||||||
|
public override Task DoSomething(Options options)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParentClass : IInterface
|
||||||
|
{
|
||||||
|
public Func<Options, Task> OnDoSomething { get; set; }
|
||||||
|
public Func<Options, Task> OnDoSomethingElse { get; set; }
|
||||||
|
|
||||||
|
public virtual Task DoSomething(Options options)
|
||||||
|
{
|
||||||
|
return OnDoSomething(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task DoSomethingElse(Options options)
|
||||||
|
{
|
||||||
|
return OnDoSomethingElse.Invoke(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
Notice a couple of things.
|
||||||
|
1. ParentClass has several virtual methods. I'm only overriding one.
|
||||||
|
2. ParentClass is setup to allow you to override the virtual methods or to just supply
|
||||||
|
it with Funcs.
|
||||||
|
3. The implementation of the virtual methods is to simply call the corresponding Funcs.
|
||||||
|
|
||||||
|
### Finding the Location of the Problem
|
||||||
|
After lots of testing and debugging, I finally figured out that somehow a FuncProxy
|
||||||
|
(see line 6 in the error stack trace above) was being injected into all of the
|
||||||
|
public `Func<,>` properties on ParentClass. Then, when methods were called that
|
||||||
|
I did not override in my ChildClass, the Func would be called in the ParentClass
|
||||||
|
and that's where Windsor was hooking in and eventually causing an error when it
|
||||||
|
eventually tried to resolve a dependency on `Task`.
|
||||||
|
|
||||||
|
### One More Clue
|
||||||
|
As I mentioned above, the presence of a TypedFactoryInterceptor in the stack trace
|
||||||
|
was strange to me. I wasn't using Windor's TypedFactoryFacility to register my ChildClass.
|
||||||
|
So, why was it sticking its nose into my class somehow?
|
||||||
|
|
||||||
|
I did some simple debugging and turned off the TypedFactoryFacility in my Windsor setup.
|
||||||
|
Just like magic,
|
||||||
|
the error I was seeing went away. So, my conclusion: somehow TypedFactoryFacility was
|
||||||
|
inserting itself into my ChildClass registration and injecting `FuncProxy`s into the
|
||||||
|
ParentClass public `Func<,>` properties.
|
||||||
|
|
||||||
|
## The Solution
|
||||||
|
This looked an aweful lot like property injection to me. I started searching related to
|
||||||
|
TypedFactoryFacility and property injection. I pretty quickly came upon this documentation:
|
||||||
|
[https://github.com/castleproject/Windsor/blob/master/docs/how-properties-are-injected.md](https://github.com/castleproject/Windsor/blob/master/docs/how-properties-are-injected.md).
|
||||||
|
|
||||||
|
That page describes how Windsor does property injection, which I actually thought was
|
||||||
|
an opt-in feature of Windsor. In fact, Windsor does it by default, as describe on that page.
|
||||||
|
|
||||||
|
Combining this revelation (to me) with the fact that TypedFactoryFacility also allows you
|
||||||
|
to take a dependency on a simple `Func` and turn it into a factory call into your Windsor
|
||||||
|
container, I concluded that, by turning on the TypedFactoryFacility in Windsor, I was opening
|
||||||
|
up public properties that had `Func<>` types to property injection by Windsor.
|
||||||
|
|
||||||
|
Now, I hate property injection, so I was fine with just turning it off completely. Luckily,
|
||||||
|
the documentation link above also included instructions how to do it. I've included that code
|
||||||
|
here.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
var propInjector = Kernel.ComponentModelBuilder
|
||||||
|
.Contributors
|
||||||
|
.OfType<PropertiesDependenciesModelInspector>()
|
||||||
|
.Single();
|
||||||
|
Kernel.ComponentModelBuilder.RemoveContributor(propInjector);
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I added this code into my Windsor container setup and tried again. Everything worked just
|
||||||
|
as expected.
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
A couple of quick takeaways:
|
||||||
|
1. Windsor does property injection by default. Check out the documentation link above for
|
||||||
|
the criteria it uses to determine when it should or not.
|
||||||
|
2. Using TypedFactoryFacility is fantastic, but turn off property injection if you have
|
||||||
|
public `Func<>` properties anywhere.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "I Like Languages That Expect Me To Be a Great Programmer"
|
||||||
|
date: 2017-01-20 20:05:00
|
||||||
|
description: "Languages that are getting too 'safe'!"
|
||||||
|
categories: [programming]
|
||||||
|
tags: []
|
||||||
|
---
|
||||||
|
|
||||||
|
A co-worker of mine recently turned me on to Uncle Bob's blog. Yup, that's Uncle Bob Martin.
|
||||||
|
|
||||||
|
Here's one of his latest posts that sparked a thought in me: [The Dark Path](http://blog.cleancoder.com/uncle-bob/2017/01/11/TheDarkPath.html).
|
||||||
|
|
||||||
|
Make sure to read his follow-up post too: [Types and Tests](http://blog.cleancoder.com/uncle-bob/2017/01/13/TypesAndTests.html).
|
||||||
|
|
||||||
|
When I got done reading those posts, this was my simple thought: I like programming languages
|
||||||
|
that expect me to be a great programmer.
|
||||||
|
|
||||||
|
Think of anyone you know who is *great* at something. Is it an artist? A politician? A teacher?
|
||||||
|
I just watched _American Sniper_ so I'm thinking of someone like Chris Kyle. He was a great
|
||||||
|
sniper. His skill with a rifle was amazing.
|
||||||
|
|
||||||
|
Now, think of why that person is considered great at what they do. Is it because what they
|
||||||
|
do is easy? Probably not. It's probably because what they do is extremely difficult, but
|
||||||
|
they still do it well. Or, it may be something that's easy to do, but very difficult to
|
||||||
|
do *really* well. Think of shooting a rifle. It's very easy to shoot a rifle. Make sure it's
|
||||||
|
loaded. Aim it somewhere (safe). Pull the trigger. That's it. But how about hitting a target
|
||||||
|
100 yards away? How about 1000 yards? Yeah, that gets really hard. Only a *great*
|
||||||
|
marksman could do that.
|
||||||
|
|
||||||
|
The reason it's hard to be a great marksman is because of the flexibility of a rifle. It's
|
||||||
|
relatively simple design lets you take it almost anywhere and lets you aim it at almost anything.
|
||||||
|
The only way to make a rifle that even a novice could use to hit targets 1000 yards away
|
||||||
|
would be to mount it on some kind of stand, add advanced aiming software somehow and let it
|
||||||
|
pull the trigger. But, that makes it hard to use, doesn't it? You now at the least have a bulky tripod to
|
||||||
|
lug around a need for a power source for the computer to aim the thing.
|
||||||
|
|
||||||
|
That's how overly restrictive programming languages have become for the sake of safety. They're
|
||||||
|
getting unwieldy. I want a flexible language that may be harder to use when I'm a novice, but
|
||||||
|
once I'm an expert, it lets me do amazing things.
|
||||||
147
src/posts/2020/05/12/psalm-130.md
Normal file
147
src/posts/2020/05/12/psalm-130.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Psalm 130"
|
||||||
|
date: 2020-05-12
|
||||||
|
description: "Psalm 130 is a plan for returning your thoughts to the Lord from being in the depths."
|
||||||
|
categories: [scripture]
|
||||||
|
tags: [psalms]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
Psalm 130 records the thoughts of a believer who goes through a remarkable transformation: from the depths of despair to proclaiming hope in the Lord to his nation.
|
||||||
|
|
||||||
|
How does this transformation happen?
|
||||||
|
|
||||||
|
This is not a psalm about a unique event or a special tragedy or an especially remarkable man accomplishing a gargantuan feat that no one can again accomplish. This is not a super-Christian battling forces of darkness with fantastic spiritual super powers.
|
||||||
|
|
||||||
|
No, instead, it is the straightforward revelation of a man's internal thoughts as they transform from deep darkness to light; of despair to hope for the future; of desperation for his own rescue to confidence that others can be rescued as well.
|
||||||
|
|
||||||
|
What we see in this psalm are the thought patterns of a mature believer; patterns we ought to emulate and repeat in our own lives.
|
||||||
|
|
||||||
|
Now, when I say "mature believer", I want to make sure you've got the same picture that I do. I don't mean someone who "has it all figured out". I definitely don't mean someone who has reached some "higher spiritual plane". All I mean is someone who has walked with the Lord for a long time and has trained his thoughts in the patterns we'll see. He falls. He fails. He's done it before and he's experienced enough to recognize it and know how to get back up again.
|
||||||
|
|
||||||
|
If we copy these patterns of thought, they will drive us to strengthen our upward confidence in the Lord and then mature us to expand our thoughts outward toward our fellow men. But, it begins where it must: our inward thoughts, thoughts directed toward ourselves.
|
||||||
|
|
||||||
|
## Inward
|
||||||
|
Verses 1-2.
|
||||||
|
|
||||||
|
The author of this psalm begins by revealing the state of his mind: his inward thoughts. He uses only two words to do this: "the depths".
|
||||||
|
|
||||||
|
### He is in trouble
|
||||||
|
By themselves "the depths" might conjure up enough for us to imagine that he is in some kind of trouble. He's not feeling good. But, this word isn't just a word for how deep a hole might be. Everywhere else it's used in Scripture, it's used to describe deep waters and especially the depths of the sea. That's another kind of "depths" altogether.
|
||||||
|
|
||||||
|
If you've ever taken a swim you can begin to imagine the analogy he is painting. You've probably felt the slight panic to get back to the surface when you stayed under the water a little too long. You've perhaps felt the water pressure when you dove down to the deep end of the pool.
|
||||||
|
|
||||||
|
But, the swimming pool is nothing like the depths of the sea. When you get down deep, the light disappears. The sunlight fades until there is only utter blackness. If help was on the way to pull you out, you couldn't see it coming. You can't even see your hand in front of your face. You're surrounded by darkness. There are no waypoints. No landmarks to recognize. No signs to point the way of escape.
|
||||||
|
|
||||||
|
All comfort of heat vanishes the deeper you sink. Before much depth, the cold becomes unbearable.
|
||||||
|
|
||||||
|
As the waters above increase, the pressure mounts until it would easily crush you.
|
||||||
|
|
||||||
|
Then, of course, you cannot breath. In the depths of the sea you are far from the surface, far from breath. You will suffocate in a matter of minutes--certainly before you make it to the top on your own.
|
||||||
|
|
||||||
|
Darkness, cold, pressure, suffocation--it paints a desperate, hopeless, life-threatening situation. This man is in great trouble.
|
||||||
|
|
||||||
|
### He admits he's in trouble
|
||||||
|
But, notice the first thought pattern we can recognize of a mature believer. It's the one important thing he does first: he admits he's in trouble. There's no denial here. There's no trying to make the situation not seem so bleak. He's in trouble he cannot escape. He knows it and he says so. This is the first thing we can note as the sign of a mature believer. The mature believer makes honest confession of his desperate need. He frankly admits his trouble and makes no attempt to hide it from himself or from the Lord.
|
||||||
|
|
||||||
|
### He calls out to the Lord
|
||||||
|
The second thought pattern of a mature believer is quickly noticed next. Admitting his situation has freed him ask for help.
|
||||||
|
|
||||||
|
What would you do if you were sinking, with no one in sight to save you?
|
||||||
|
|
||||||
|
I think my first instinct would be to call out as loudly as I could for someone--anyone at all--to come rescue me!
|
||||||
|
|
||||||
|
My second oldest, Gideon, is not quite able to dress himself yet. When he gets stuck in his shirt or can't quite work the button on his pants, he has the habit of desperately calling out "Somebody! Anybody!". He doesn't care if me or Grace or even his brother (sometimes) helps him. He just needs someone to get this shirt off!
|
||||||
|
|
||||||
|
But, this is not exactly what he does here. He cries out. But he specifically calls out to only one person: the Lord. And there's no doubt who he means. He uses the name of the Lord, Yahweh. He pleads with Him to hear his voice, to hear the voice of his supplications; or, "pleas for mercy" as the ESV translates it.
|
||||||
|
|
||||||
|
It seems counterintuitive to call out for a specific person. If he's truly desperate wouldn't any help be welcome, no matter who it came from? But, this too is a thought pattern of a mature believer. He knows the One who not only can save him, but is willing to save him. There is only One such person and He is the Lord.
|
||||||
|
|
||||||
|
He is the one who will put it on the heart of your spouse to say the right kind word at the right time. Or burden a preacher with an exhortation from the Word that encourages you. Or embolden your brother to give you the strong admonition you need.
|
||||||
|
|
||||||
|
An interesting thing happens next. From a dire situation, his thoughts begin to shift. His calling out to the Lord has shifted his focus from his circumstances to the Lord Himself. He now begins to drift away from his inward thoughts and toward upward thoughts.
|
||||||
|
|
||||||
|
## Upward
|
||||||
|
Verses 3-6.
|
||||||
|
|
||||||
|
Notice how his thoughts are still on himself, but no longer inward. His thoughts have turned to who he is in light of his relationship to a holy God. They have turned upward.
|
||||||
|
|
||||||
|
### Sin and Forgiveness
|
||||||
|
What would it be like if the Lord marked our iniquities? What if he kept a record and closely watched them? It's obvious--our case would be hopeless! We'd have no chance of standing at that judgment.
|
||||||
|
|
||||||
|
Think of the things this reveals about his view of God in relation to himself:
|
||||||
|
- First, he knows he has sinned and does not hide it
|
||||||
|
- He knows his sin is serious, and it has fearful consequences
|
||||||
|
- He also knows the Lord knows these sins
|
||||||
|
- Beyond this, he knows he ought to be rightfully accountable to the Lord for these sins
|
||||||
|
- But, he also believes that the Lord does not keep a record of these sins
|
||||||
|
- More than that, he knows it's not a matter of the Lord misplacing the official sin record, but the marks are gone because there is forgiveness with the Lord
|
||||||
|
- As a result of this forgiveness, he fears the Lord
|
||||||
|
|
||||||
|
So, we can notice the third and fourth thought patterns of a mature believer.
|
||||||
|
|
||||||
|
First, he knows he is a sinner, forgiven by a loving God. This is how he stands, even though he has sinned. This is both a humbling admission and a strengthening encouragement and they are in a kind of balance. Thinking only of ourselves as sinners neglects the truth of God's work to forgive us. Thinking only of the forgiveness we have in the Lord neglects the truth of our sinfulness that necessitated His work in the first place.
|
||||||
|
|
||||||
|
Second, rather than a license to sin, this forgiveness leads him to fall fearfully before the Lord in humble worship. Perhaps, like me, you don't normally associate God's forgiveness with fearing Him. I, for one, am much more likely to associate it with relief or gratefulness or happiness.
|
||||||
|
|
||||||
|
But the connection is perhaps closer than we think. When we acknowledge that God is the one who forgives our sin, at the same time, we're acknowledging that He has the authority and the power to do so. The natural reaction to such a thought is to fear, follow and worship such a God who wields such power over us, but choses instead to forgive us.
|
||||||
|
|
||||||
|
The forgiveness of the Lord also frees a man to leave the things behind him (his sins, weaknesses and failures) and press on in the work of the Lord. In other words, it leads to a greater desire to follow the Lord. As we follow Him, we know Him better. As we know Him better, we can only fear Him more. This certainly seemed to be Paul's mindset in Philippians 3:
|
||||||
|
|
||||||
|
> **Philippians 3:12–17**
|
||||||
|
>
|
||||||
|
> 12 Not that I have already obtained it or have already become perfect, but I press on so that I may lay hold of that for which also I was laid hold of by Christ Jesus.
|
||||||
|
> 13 Brethren, I do not regard myself as having laid hold of it yet; but one thing I do: forgetting what lies behind and reaching forward to what lies ahead,
|
||||||
|
> 14 I press on toward the goal for the prize of the upward call of God in Christ Jesus.
|
||||||
|
> 15 Let us therefore, as many as are perfect, have this attitude; and if in anything you have a different attitude, God will reveal that also to you;
|
||||||
|
> 16 however, let us keep living by that same standard to which we have attained.
|
||||||
|
> 17 Brethren, join in following my example, and observe those who walk according to the pattern you have in us.
|
||||||
|
|
||||||
|
Notice the connection and progression from leaving what lays behind to striving for the upward call to keeping an attitude of holy living.
|
||||||
|
|
||||||
|
### Waiting and Hoping
|
||||||
|
Earlier, in verses 1-2, the psalmist honestly admits his circumstances and this leads him to action: to call out to the Lord for rescue. Here too, having turned his thoughts upward to the Lord's forgiveness of his sins, he now takes action. He trains his thoughts on waiting for the Lord and hoping in His word.
|
||||||
|
|
||||||
|
There are at least two things to notice in these two verses. First, the repetition and second, the comparison to the watchmen.
|
||||||
|
|
||||||
|
It's interesting how he repeats, to himself, how he will wait for the Lord. It's almost as if he needs more than one reminder to wait for Him. Perhaps he is retraining his thoughts away from fearfulness and despair to an expectant, waiting hopefulness.
|
||||||
|
|
||||||
|
Keep in mind that we have no indication that his circumstances have changed at all. But his thinking is brightening considerably. His concentration is no longer on his circumstances, but on the Lord who certainly will rescue him.
|
||||||
|
|
||||||
|
Second, we should ask ourselves what the waiting of a watchman for the morning is like. Even for a new watchman, on his first day on the job, I can't imagine he has any question or uncertainty about the morning coming! There is a confidence that the sun will rise and the watch will end.
|
||||||
|
|
||||||
|
The fifth thought pattern of a mature believer now emerges. He trains his thoughts to confidently hope in the Lord, waiting on His rescue, no matter the circumstances.
|
||||||
|
|
||||||
|
As before, now another interesting things happens. As he moves away from inward thinking to upward thinking, he grows in his confidence to trust the Lord as he remembers the lovingkindness of the Lord. In that confidence his thoughts now turn completely away from himself and outward, towards his fellow countrymen.
|
||||||
|
|
||||||
|
## Outward
|
||||||
|
Verses 7-8.
|
||||||
|
|
||||||
|
Now, the mood of the psalmist has completely changed from where he started. Far from crying out in desperation for help, he is now calling out to his countrymen to hope in the Lord!
|
||||||
|
|
||||||
|
I think we can notice two more thought patterns of the psalmist to emulate.
|
||||||
|
|
||||||
|
### Seeing the needs of others
|
||||||
|
The first is a sense of responsibility to share his confidence in the Lord with others.
|
||||||
|
His thinking has been so transformed that, though we still have no indication that he has been rescued, he sees the need of his fellow man for the same trusting hope in the Lord. His own issues are no longer the only ones he sees. Others are in similar circumstances, similar "depths". So, he calls on them to hope based on two characteristics of the Lord: His lovingkindness and his abundant redemption.
|
||||||
|
|
||||||
|
### Confidence in the future
|
||||||
|
The second is a confidence in the future actions of the Lord.
|
||||||
|
|
||||||
|
How does he know that God will redeem Israel from all his iniquities?
|
||||||
|
|
||||||
|
I think the answer is relatively simple. He has seen the past actions of the Lord in his life. He has observed the character of the Lord worked out in his own life. Having drawn closer to the Lord, he knows Him better and can look out into the future, confident that the Lord will do the same then as He has done in the past.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
To summarize this psalm, I think we could say this. The mature believer is not someone who has reached a higher plane of Christian living, never finding himself in the depths, but always thinking upward and outward. I'm confident that every believer finds himself in the depths at some time or another and most likely even quite regularly.
|
||||||
|
|
||||||
|
Remember, the one writing this psalm, who I see as a very mature believer, is one who has fallen! But remember what Proverbs 24:16 says:
|
||||||
|
|
||||||
|
> **Proverbs 24:16**
|
||||||
|
>
|
||||||
|
> 16 For a righteous man falls seven times, and rises again, But the wicked stumble in time of calamity.
|
||||||
|
|
||||||
|
Rising again is what the mature believer does. But how? That is what this Psalm spells out for us.
|
||||||
|
|
||||||
|
The mature believer is the one whose has learned certain patterns of thought to a consistent way of thinking that does one simple thing: it always turns him to the Lord for rescue. This then enables him to grow his thoughts from necessary introspection (those inward thoughts), upward, to the joy of the reality of his relationship with the Lord. It also emboldens him to reach outward to share with those around him the bounty of hope and lovingkindness available in the Lord.
|
||||||
462
src/posts/2020/05/27/iterating-all-catalog-metaobjects.md
Normal file
462
src/posts/2020/05/27/iterating-all-catalog-metaobjects.md
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Iterating All EPiServer Catalog MetaObjects"
|
||||||
|
date: 2020-05-27
|
||||||
|
description: "C# iterators to walk through all EPiServer catalog node and entry content meta objects."
|
||||||
|
categories: [programming]
|
||||||
|
tags: [episerver,csharp,.net]
|
||||||
|
---
|
||||||
|
|
||||||
|
If you're maintaining an e-commerce-enabled site in EPiServer and you're using the built-in catalog, then
|
||||||
|
you have probably found the need to incorporate external data into the catalog, perhaps from various sources.
|
||||||
|
|
||||||
|
Such is the case for me on an EPiServer site I've been working on over the last (almost) four years. The primary
|
||||||
|
data for the catalog comes into EPiServer from an external PIM system. But, then there is all sorts of ancillary
|
||||||
|
data that needs to calculated from existing fields or pulled from external sources. These external data sources
|
||||||
|
don't really provide a way to incrementally apply deltas for the data they provide. The only option to make updates is to walk
|
||||||
|
through the entire catalog and see if anything needs updating when matched against the external data I've pulled.
|
||||||
|
|
||||||
|
This external data is stored in the catalog as custom EPiServer MetaFields. So,
|
||||||
|
a need arose to easily walk through the entire catalog, pick up every MetaObject (to update the custom MetaFields)
|
||||||
|
in every available catalog culture and make the appropriate updates.
|
||||||
|
|
||||||
|
Below are the iterators I wrote to accomplish this. They let you walk the entire catalog tree (in a breadth-first manner)
|
||||||
|
starting from the top as if you were iterating a simple C# array or list.
|
||||||
|
|
||||||
|
## High-Level Usage
|
||||||
|
Because all we're doing is writing iterators, the high-level usage looks just like you were interating through
|
||||||
|
an `IEnumerable` in C# like an array or a `List<T>`.
|
||||||
|
|
||||||
|
The iterators are implemented as extension methods on the `ICatalogSystem` interface. So, iterating through all
|
||||||
|
of the catalog entry MetaObjects looks like this.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
using Mediachase.Commerce.Catalog;
|
||||||
|
using Mediachase.MetaDataPlus;
|
||||||
|
|
||||||
|
ICatalogSystem catalog = CatalogContext.Current;
|
||||||
|
MetaDataContext mdc = MetaDataContext.Instance;
|
||||||
|
|
||||||
|
foreach ((MetaObject metaObject, MetaDataContext metaDataContext) entryMO in catalog.AllCatalogSystemEntryMetaObjects(mdc))
|
||||||
|
{
|
||||||
|
entryMO.metaObject["SomeCustomMetaFieldName"] = "external data source value here";
|
||||||
|
entryMO.metaObject.AcceptChanges(entryMO.metaDataContext);
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I implemented the separate iterators for NodeContent MetaObjects and the EntryContent
|
||||||
|
MetaObjects. You easily use them in combination to iterate through the MetaObjects of the entire catalog tree.
|
||||||
|
|
||||||
|
Note a couple of things from the example code above:
|
||||||
|
1. You have to give the iterator the MetaDataContext. As we'll see in the iterator code, it will copy this MDC for each catalog language so that you get a MetaObject for every culture present in your catalog.
|
||||||
|
1. Because of #1 above, the current MetaDataContext is passed to you during iteration so that you know which catalog culture this MetaObject is for.
|
||||||
|
|
||||||
|
## Extension Method Code
|
||||||
|
The extension method code for AllCatalogSystemEntryMetaObjects is pretty simple. It uses four iterators in nested foreach loops to loop through
|
||||||
|
1. All catalogs
|
||||||
|
1. All nodes
|
||||||
|
1. All entries
|
||||||
|
1. All meta objects
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public static IEnumerable<(MetaObject metaObject, MetaDataContext metaDataContext)> AllCatalogSystemEntryMetaObjects(
|
||||||
|
this ICatalogSystem catalogSystem,
|
||||||
|
MetaDataContext metaDataContext)
|
||||||
|
{
|
||||||
|
foreach (var catalog in new CatalogSystemCatalogs(catalogSystem))
|
||||||
|
{
|
||||||
|
foreach (var node in new CatalogSystemNodes(catalogSystem, catalog.CatalogId))
|
||||||
|
{
|
||||||
|
foreach (var entry in new CatalogSystemEntries(catalogSystem, catalog.CatalogId, node.CatalogNodeId))
|
||||||
|
{
|
||||||
|
foreach (var tuple in new CatalogSystemEntryMetaObjects(metaDataContext, catalog, entry))
|
||||||
|
{
|
||||||
|
yield return tuple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I broke the iteration down into separate iterators in this fashion so that I could easily iterate through all the entries (for example) in a single node, if I knew from the beginning which node I wanted to start at. If that was the case, I could easily write another extension method that took a node ID (or node code) as a parameter, use the ICatalogSystem to find that node and then pass the node ID to the CatalogSystemEntries iterator to iterate just the entries under that node.
|
||||||
|
|
||||||
|
## Iterators
|
||||||
|
We have four iterators in use:
|
||||||
|
1. CatalogSystemCatalogs - iterates through all catalogs
|
||||||
|
1. CatalogSystemNodes - iterates through all nodes, in a breadth-first fashion, in a catalog
|
||||||
|
1. CatalogSystemEntries - iterates through all entries in a node
|
||||||
|
1. CatalogSystemEntryMetaObjects - iterates through all MetaObjects for an entry
|
||||||
|
|
||||||
|
### CatalogSystemCatalogs Iterator
|
||||||
|
The code is pretty simple. We implement an IEnumerable which uses our implementation of an IEnumerator.
|
||||||
|
|
||||||
|
The IEnumerator simply gets all the catalogs from the ICatalogSystem and, as you call MoveNext() (usually done for you by the foreach loop), increments an index value to give you the current CatalogDto.CatalogRow to work with.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class CatalogSystemCatalogs : IEnumerable<CatalogDto.CatalogRow>
|
||||||
|
{
|
||||||
|
private readonly CatalogSystemCatalogEnumerator _enumerator;
|
||||||
|
|
||||||
|
public CatalogSystemCatalogs(ICatalogSystem catalogSystem)
|
||||||
|
{
|
||||||
|
_enumerator = new CatalogSystemCatalogEnumerator(catalogSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<CatalogDto.CatalogRow> GetEnumerator() => _enumerator;
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSystemCatalogEnumerator : IEnumerator<CatalogDto.CatalogRow>
|
||||||
|
{
|
||||||
|
private int _currentIndex = 0;
|
||||||
|
private readonly ICatalogSystem _catalogSystem;
|
||||||
|
private Lazy<Lst<CatalogDto.CatalogRow>> _catalogs;
|
||||||
|
|
||||||
|
public CatalogSystemCatalogEnumerator(ICatalogSystem catalogSystem)
|
||||||
|
{
|
||||||
|
_catalogSystem = catalogSystem;
|
||||||
|
_catalogs = new Lazy<Lst<CatalogDto.CatalogRow>>(GetCatalogs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CatalogDto.CatalogRow Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _catalogs.Value[_currentIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_catalogs.IsValueCreated)
|
||||||
|
{
|
||||||
|
_currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentIndex >= _catalogs.Value.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_catalogs = new Lazy<Lst<CatalogDto.CatalogRow>>(GetCatalogs);
|
||||||
|
_currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lst<CatalogDto.CatalogRow> GetCatalogs()
|
||||||
|
=> _catalogSystem.GetCatalogDto().Catalog.Freeze();
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
### CatalogSystemNodes Iterator
|
||||||
|
For the nodes iterator, things get a little more complicated. Nodes can be nested, so we have tree structure we have to navigate. We use a Queue to walk through the nodes in a breadth-first fashion.
|
||||||
|
|
||||||
|
I like to use as many functional programming techniques as I can in C# these days. I use the great [LanguageExt](https://github.com/louthy/language-ext) library for this. This library is where the `Que<T>`, `Option<T>` and `Lst<T>` data structures come from. I don't want to go in depth about what they are, but to understand the code below, you should now this: `Que<T>` is an immutable `Queue<T>`, `Lst<T>` is an immutable `List<T>` and `Option<T>` is basically the functional way to do the null-object pattern (instead of dealing with nulls, use an object to represent "I don't have a value").
|
||||||
|
|
||||||
|
If you're not familiar with breadth-first tree traversals, it's not too complicated. What we do first is get all the nodes at the top level (in our case, all the nodes that are direct children of the catalog we pass in) and put them in the queue. Then, we iterate through each of the nodes we put in the queue and add their child nodes to the queue. We do this until we run out of nodes to process in the queue which indicates we've gone through all nodes in the tree.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class CatalogSystemNodes : IEnumerable<CatalogNodeDto.CatalogNodeRow>
|
||||||
|
{
|
||||||
|
private readonly CatalogSystemNodeEnumerator _enumerator;
|
||||||
|
|
||||||
|
public CatalogSystemNodes(ICatalogSystem catalogSystem, int catalogId, int startingParentNodeId = 0)
|
||||||
|
{
|
||||||
|
_enumerator = new CatalogSystemNodeEnumerator(catalogSystem, catalogId, startingParentNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<CatalogNodeDto.CatalogNodeRow> GetEnumerator() => _enumerator;
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSystemNodeEnumerator : IEnumerator<CatalogNodeDto.CatalogNodeRow>
|
||||||
|
{
|
||||||
|
private Que<int> _nodeQue;
|
||||||
|
private readonly ICatalogSystem _catalogSystem;
|
||||||
|
private readonly int _catalogId;
|
||||||
|
private readonly int _startingParentNodeId;
|
||||||
|
private Option<CatalogNodeDto.CatalogNodeRow> _current;
|
||||||
|
|
||||||
|
// We have a parameter for the starting node ID (startingParentNodeId) which defaults to 0. If you
|
||||||
|
// pass in a node ID, we'll start iterating child nodes of that node. If you don't pass a value (keep
|
||||||
|
// the default of 0), then we'll iterate through all nodes in the catalog.
|
||||||
|
public CatalogSystemNodeEnumerator(ICatalogSystem catalogSystem, int catalogId, int startingParentNodeId = 0)
|
||||||
|
{
|
||||||
|
_catalogSystem = catalogSystem;
|
||||||
|
_catalogId = catalogId;
|
||||||
|
_startingParentNodeId = startingParentNodeId;
|
||||||
|
_nodeQue = Prelude.Queue(_startingParentNodeId);
|
||||||
|
_current = Option<CatalogNodeDto.CatalogNodeRow>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueUnsafe() lets us get the value of _current (which is an Option<T>)
|
||||||
|
// or null if the Option<T> has no value.
|
||||||
|
public CatalogNodeDto.CatalogNodeRow Current => _current.ValueUnsafe();
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
// end-case scenario--we've run out of nodes in the queue, so we're
|
||||||
|
// done traversing the node tree
|
||||||
|
if (!_nodeQue.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextNodeId = GetNextNodeIdAndUpdateQue();
|
||||||
|
|
||||||
|
// special starting condition when we want to do the entire tree
|
||||||
|
// and no other starting node was passed in
|
||||||
|
if (nextNodeId == 0)
|
||||||
|
{
|
||||||
|
// special case of there being a catalog node with no category nodes beneath it
|
||||||
|
if (!_nodeQue.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nextNodeId = GetNextNodeIdAndUpdateQue();
|
||||||
|
}
|
||||||
|
|
||||||
|
_current = GetNode(nextNodeId);
|
||||||
|
|
||||||
|
return _current.IsSome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetNextNodeIdAndUpdateQue()
|
||||||
|
{
|
||||||
|
// get the next node ID from the queue
|
||||||
|
int nextNodeId = _nodeQue.Peek();
|
||||||
|
// remove the retrieved node from the queue
|
||||||
|
_nodeQue = _nodeQue.Dequeue();
|
||||||
|
// get all of this node's children and add their IDs to the queue
|
||||||
|
_nodeQue = GetChildren(nextNodeId)
|
||||||
|
.Fold(_nodeQue, (q, nodeRow) => q.Enqueue(nodeRow.CatalogNodeId));
|
||||||
|
|
||||||
|
return nextNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Option<CatalogNodeDto.CatalogNodeRow> GetNode(int nodeId)
|
||||||
|
=> _catalogSystem.GetCatalogNodeDto(nodeId).CatalogNode.HeadOrNone();
|
||||||
|
|
||||||
|
private Lst<CatalogNodeDto.CatalogNodeRow> GetChildren(int nodeId)
|
||||||
|
=> _catalogSystem.GetCatalogNodesDto(_catalogId, nodeId).CatalogNode.Freeze();
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_nodeQue = Prelude.Queue(_startingParentNodeId);
|
||||||
|
_current = Option<CatalogNodeDto.CatalogNodeRow>.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
### CatalogSystemEntries Iterator
|
||||||
|
Since entries cannot be arrange in a nested fashion, this iterator is a little less complicated than the nodes iterator. All we do is take in a catalogId and a catalogNodeId and retrieve all the entries below the given node ID, passing them to you one at a time as you call MoveNext().
|
||||||
|
|
||||||
|
The only mildly complex thing here is that we use a `Lazy<T>` so that we're not retrieving the entries immediately when you instanstiate the IEnumerator. We only load the entries when you MoveNext() and access Current which indicates you really do want some entries.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class CatalogSystemEntries : IEnumerable<CatalogEntryDto.CatalogEntryRow>
|
||||||
|
{
|
||||||
|
private readonly CatalogSystemEntryEnumerator _enumerator;
|
||||||
|
|
||||||
|
public CatalogSystemEntries(ICatalogSystem catalogSystem, int catalogId, int catalogNodeId)
|
||||||
|
{
|
||||||
|
_enumerator = new CatalogSystemEntryEnumerator(catalogSystem, catalogId, catalogNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<CatalogEntryDto.CatalogEntryRow> GetEnumerator() => _enumerator;
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSystemEntryEnumerator : IEnumerator<CatalogEntryDto.CatalogEntryRow>
|
||||||
|
{
|
||||||
|
private int _currentIndex = 0;
|
||||||
|
private readonly ICatalogSystem _catalogSystem;
|
||||||
|
private Lazy<Lst<CatalogEntryDto.CatalogEntryRow>> _entries;
|
||||||
|
private readonly int _catalogNodeId;
|
||||||
|
private readonly int _catalogId;
|
||||||
|
|
||||||
|
// we take in the catalogNodeId to start at so that you could use this IEnumerator
|
||||||
|
// to collect entries at any starting node in the catalog.
|
||||||
|
public CatalogSystemEntryEnumerator(ICatalogSystem catalogSystem, int catalogId, int catalogNodeId)
|
||||||
|
{
|
||||||
|
_catalogSystem = catalogSystem;
|
||||||
|
// we use a Lazy<T> object here so that we don't do a database call
|
||||||
|
// as soon as you instantiate this class. We only do it once you
|
||||||
|
// access Current, indicating you really want to iterate the entries now
|
||||||
|
_entries = new Lazy<Lst<CatalogEntryDto.CatalogEntryRow>>(GetEntries);
|
||||||
|
_catalogNodeId = catalogNodeId;
|
||||||
|
_catalogId = catalogId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CatalogEntryDto.CatalogEntryRow Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _entries.Value[_currentIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
// the first MoveNext() call will be when when _entries doesn't have
|
||||||
|
// a value yet (but will after Current is accessed). So, this leaves
|
||||||
|
// the _currentIndex at 0 the first time MoveNext() is called.
|
||||||
|
if (_entries.IsValueCreated)
|
||||||
|
{
|
||||||
|
_currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we just incremented _currentIndex to the count of the _entries list,
|
||||||
|
// so we return false to indicate there are no more values. We really could
|
||||||
|
// use == here instead of >=. I guess >= feels safer for some reason.
|
||||||
|
if (_currentIndex >= _entries.Value.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_entries = new Lazy<Lst<CatalogEntryDto.CatalogEntryRow>>(GetEntries);
|
||||||
|
_currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lst<CatalogEntryDto.CatalogEntryRow> GetEntries()
|
||||||
|
=> _catalogSystem.GetCatalogEntriesDto(_catalogId, _catalogNodeId).CatalogEntry.Freeze();
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
### CatalogSystemEntryMetaObjects Iterator
|
||||||
|
The MetaObjects iterator is a different than the other iterators in that it doesn't really iterate through a list or tree of MetaObjects. Instead, it iterates through all fo the MetaObject instances for a particular entry--one instance for reach culture available for the catalog.
|
||||||
|
|
||||||
|
So, as you'll see in the code below, one of the first things we do is grab all of the languages for the catalog so that we know what we need to interate through.
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
public class CatalogSystemEntryMetaObjects : IEnumerable<(MetaObject metaObject, MetaDataContext metaDataContext)>
|
||||||
|
{
|
||||||
|
private readonly CatalogSystemEntryMetaObjectsEnumerator _enumerator;
|
||||||
|
|
||||||
|
public CatalogSystemEntryMetaObjects(MetaDataContext metaDataContext, CatalogDto.CatalogRow catalog, CatalogEntryDto.CatalogEntryRow entry)
|
||||||
|
{
|
||||||
|
_enumerator = new CatalogSystemEntryMetaObjectsEnumerator(metaDataContext, catalog, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<(MetaObject metaObject, MetaDataContext metaDataContext)> GetEnumerator() => _enumerator;
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSystemEntryMetaObjectsEnumerator : IEnumerator<(MetaObject metaObject, MetaDataContext metaDataContext)>
|
||||||
|
{
|
||||||
|
private int _currentIndex = 0;
|
||||||
|
private readonly CatalogEntryDto.CatalogEntryRow _entry;
|
||||||
|
private readonly MetaDataContext _metaDataContext;
|
||||||
|
private Lazy<Lst<CultureInfo>> _catalogLanguages;
|
||||||
|
private readonly CatalogDto.CatalogRow _catalog;
|
||||||
|
|
||||||
|
public CatalogSystemEntryMetaObjectsEnumerator(MetaDataContext metaDataContext,
|
||||||
|
CatalogDto.CatalogRow catalog,
|
||||||
|
CatalogEntryDto.CatalogEntryRow entry)
|
||||||
|
{
|
||||||
|
_catalog = catalog;
|
||||||
|
_entry = entry;
|
||||||
|
_metaDataContext = metaDataContext;
|
||||||
|
_catalogLanguages = new Lazy<Lst<CultureInfo>>(GetCatalogLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public (MetaObject metaObject, MetaDataContext metaDataContext) Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// what we're really iterating through is the catalog languages.
|
||||||
|
// as we iterate through them, we grab the associate MetaObject for the current
|
||||||
|
// language for the given entry
|
||||||
|
CultureInfo currentCultureInfo = _catalogLanguages.Value[_currentIndex];
|
||||||
|
|
||||||
|
var mdc = _metaDataContext.Clone();
|
||||||
|
mdc.UseCurrentThreadCulture = false;
|
||||||
|
mdc.Language = currentCultureInfo.Name;
|
||||||
|
|
||||||
|
return (MetaObject.Load(mdc, _entry.CatalogEntryId, _entry.MetaClassId), mdc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
// As with the Lazy<T> for iterating through the entries, the first tie MoveNext() is called
|
||||||
|
// the _catalogLanguages Lazy<T> will not have a value. It will after the first Current access.
|
||||||
|
// So, for the first MoveNext(), we want to keep the _currentIndex at 0, which is what this if-statement
|
||||||
|
// accomplishes.
|
||||||
|
if (_catalogLanguages.IsValueCreated)
|
||||||
|
{
|
||||||
|
_currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentIndex >= _catalogLanguages.Value.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_catalogLanguages = new Lazy<Lst<CultureInfo>>(GetCatalogLanguages);
|
||||||
|
_currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lst<CultureInfo> GetCatalogLanguages()
|
||||||
|
=> _catalog.GetCatalogLanguageRows()
|
||||||
|
.Map(lr => new CultureInfo(lr.LanguageCode))
|
||||||
|
.Freeze();
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
Even though the above code will give you the convenience of iterating through your entire EPiServer catalog(s), be warned! You could easily forget what the IEnumerators really do above and do something like this:
|
||||||
|
|
||||||
|
{% highlight csharp %}
|
||||||
|
// REALLY BAD CODE, DON'T DO
|
||||||
|
List<(MetaObject metaObject, MetaDataContext mdc)> allTheMetaObjects = _catalogSystem.AllCatalogSystemEntryMetaObjects(mdc).ToList();
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
I hope you see the danger of this. If you have a large catalog, you're loading the ENTIRE set of MetaObjects into memory in your `allTheMetaObjects` list. This probably won't be good for your application. If you have a small catalog, this might not be a big deal. But, keep this in mind!
|
||||||
16
src/posts/organize.ps1
Normal file
16
src/posts/organize.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
$posts = gci ./ -filter *.md
|
||||||
|
|
||||||
|
$posts | % {
|
||||||
|
$filename = $_.Name
|
||||||
|
$parts = $filename.split("-")
|
||||||
|
$year = $parts[0]
|
||||||
|
$month = $parts[1]
|
||||||
|
$day = $parts[2]
|
||||||
|
|
||||||
|
$filename = $filename -replace "$year-$month-$day-",""
|
||||||
|
$newDir = "./$year/$month/$day"
|
||||||
|
|
||||||
|
new-item $newDir -itemtype directory -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
move-item $_.FullName "$newDir/$filename"
|
||||||
|
}
|
||||||
13
src/templates/page.js
Normal file
13
src/templates/page.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Layout from "../components/layout"
|
||||||
|
|
||||||
|
export default function Page({ children }) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<article class="page">
|
||||||
|
<h1 class="page-title">page.title</h1>
|
||||||
|
{children}
|
||||||
|
</article>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/templates/post.js
Normal file
71
src/templates/post.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { graphql } from "gatsby";
|
||||||
|
import Layout from "../components/layout";
|
||||||
|
import { DiscussionEmbed } from 'disqus-react';
|
||||||
|
|
||||||
|
export default function Post({ data }) {
|
||||||
|
const post = data.markdownRemark;
|
||||||
|
|
||||||
|
var tags = post.frontmatter.tags.map((tag) => {
|
||||||
|
return (<a href={'/tags/' + tag }>#{tag}</a>);
|
||||||
|
});
|
||||||
|
var categories = post.frontmatter.categories.map((category) => {
|
||||||
|
return (<a href={'/categories/' + category }>#{category}</a>);
|
||||||
|
});
|
||||||
|
var postDate = new Date(post.frontmatter.date);
|
||||||
|
console.log(postDate);
|
||||||
|
var isoDate = postDate.toISOString();
|
||||||
|
var visibleDate = postDate.toDateString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div>
|
||||||
|
<article className="post">
|
||||||
|
<h1 className="post-title">{post.frontmatter.title}</h1>
|
||||||
|
<div className="post-info d-flex justify-content-start">
|
||||||
|
<time datetime={isoDate} className="post-date">{visibleDate}</time>
|
||||||
|
<nav className="post-tags">
|
||||||
|
{tags}
|
||||||
|
</nav>
|
||||||
|
<nav className="post-categories">
|
||||||
|
<span>posted in:</span>
|
||||||
|
{categories}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: post.html }} />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<DiscussionEmbed
|
||||||
|
shortname='fishybusiness'
|
||||||
|
config={
|
||||||
|
{
|
||||||
|
url: "https://" + data.site.host + ":" + data.site.port,
|
||||||
|
identifier: post.slug,
|
||||||
|
title: post.frontmatter.title,
|
||||||
|
language: 'en_US'
|
||||||
|
}
|
||||||
|
}/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const query = graphql`
|
||||||
|
query($slug: String!) {
|
||||||
|
markdownRemark(fields: { slug: { eq: $slug } }) {
|
||||||
|
html
|
||||||
|
|
||||||
|
frontmatter {
|
||||||
|
title
|
||||||
|
date
|
||||||
|
categories
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
site {
|
||||||
|
host
|
||||||
|
port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
Reference in New Issue
Block a user