Sunday, May 20, 2007

(ZT)学做 Presentation

Daphne所属的公司,为 EarthSound 拟定了一份打入台湾市场的行销企划。待众人坐定之后,Daphne带着从容和悦的神情,起身上台开始了这场presentation。 Good afternoon and thank you for coming today. My name is Daphne Fu, and I'm the Project Manager in Taipei for Jackson&Wang. I'm here today to present our firm's marketing plan, which is designed to introduce EarthSound's products to the wealthy Taiwan market. Our research shows that there are big profits waiting in Taiwan, so we're excited at the opportunities we see for EarthSound. I hope that some of our excitement will rub off on all of you. 下午好,感谢各位今天的莅临,我是Daphne傅,Jackson&Wang公司台北地区的项目经理。今天在这里向各位说明本公司的行销企划,是针对贵公司将产品打入富裕的台湾市场所拟定的。根据我们的调查,台湾市场潜藏着丰厚的利润,对于"天籁"所面对的机会,我们感到十分兴奋,希望能把这种感觉传达给在座的各位。 I'll start with a few facts and figures about the health and beauty products market in Taiwan. Next, I'll go over the standard types of advertising that have been successful for these products in Taiwan. Finally, I'll analyze current opportunities and give a few recommendations. A booklet of the marketing plan will be handed out after the presentation, and it will give you all the details. 我首先会报告一些实际情况与数据,有关台湾健康美容用品的市场;然后,再说明典型的成功广告案例,有关此类产品打入台湾市场的情形。最后,我会分析目前的机会并提出几点建议。简报之后,我将发给各位一本关于这份行销计划的报告书,里面写得非常详细。 Because we all have tight schedules, I'd like to introduce the first point, the current state of Taiwan's health and beauty market... 我知道大家都很忙,我想马上就开始介绍今天的第一项主题:台湾健康美容用品的市场现况…… Notes: 1. rub off on 沾到;感染 Rub 原是“磨擦”的意思,而 rub off 则指“擦掉”;rub off on是美式用法,它的意思是“把(某物)擦掉,再把它抹于另一件物品上”,引申有“将个人想法、行为影响了他人或把情绪感染给别人”的喻意。值得注意的是,这个词组是以物为主语,而不是人,请参考本文和下例。 Since you're new to the team, I hope some of my confidence will rub off on you. 你是这个团队的新成员,希望我的信心能感染你。 2. go over 浏览;仔细检查这个词组有两层意思:一是指把某个文件看过一下或事情讨论一遍,只要得到一个基本概念就好;另一个意思则是精密地审查。 Let's go over some ways to make our office more efficient. 让我们很快地讨论几个使办公室更有效率的办法。 句型总结 ●  说明简报目的 1. I'm here today to present our firm's marketing plan. 2. My purpose today is to present our firm's marketing plan. 3. My goal for this meeting is to present our firm's marketing plan. 4. The aim of this presentation is to present our firm's marketing plan. 5. The reason why I'm here today is to present our firm's marketing plan. Presenter一站上讲台,不论下面的听众是否知道今天的主题,都要先介绍presentation的目的,这样不仅可以帮助听众了解下面要讲的内容,也是一个最普遍、正式的开场白。"I am here today to..."就是个非常简洁实用的句型。 ●  逐项说明大纲 1. I'll start with... Next,... Finally,... 2. First,... Second,... Third (and last) 3. To begin with... Moving right along,... To close,... 4. I'll open with... Then I'll... And I'll wrap up with... 5. Step one is to... After that we'll... And our final step is to... 说明presentation的目的之后,接下来应该把presentation的大纲按照顺序告诉听众,帮助听众掌握内容的架构。而介绍大纲的关键是要条理分明、次序清楚。例如文中所用的"I'll start with...(一开始是……)Next,...(接下来是……)Finally,...(最后是……)",就是一个很好的例子。 ●  进入正题 1. I'd like to introduce the first point - the current state of Taiwan's health and beauty market. 2. Now I'll go over the first point - the current state of Taiwan's health and beauty market. 3. Now I'll review the first point - the current state of Taiwan's health and beauty market. 4. Now I'm going to consider the first point - the current state of Taiwan's health and beauty market. 5. It's time to discuss the first point - the current state of Taiwan's health and beauty market. 在开始讨论不同的主题之前,还是要把该段内容的重点提一下,这样可以帮助自己和听众有条理地听和说。I'd like to introduce... 是个很普遍的句型,可以清楚的告知观众,就要进入某个主题了。 特别提示 讲台之于演说者有如舞台之于演员。舞台上有些位置会使您成为观众瞩目的焦点;而有些位置却会让观众视而不见。聪明的演说者应学习站在正确的角度上,以适当的位置面对您的观众。 当演讲者做presentation时,讲台放置的位置以中央优于左右两旁,前方(靠近观众)胜于后方,左方又胜过右方。 在presentation时,若是以演讲者为主,则屏幕位置应放置在演讲者的右后侧,这就构成了"人为主,屏幕为辅"的效果。 在presentation时,无论是您的右半边脸较好看,或是左半边脸较迷人,正面整个面对观众是最强势的位置,其次是正面75度角,再而半侧面,背对则最弱。

 

 

做presentation时,若能提供看得见的数据,再给观众一些具体可及的线索,那么这场presentation就成功一半了。现在,Daphne就准备了一些醒目有力的幻灯片来增强presentation的效果。

I have prepared a short slide presentation to give you a picture of the Taiwanese market. Please direct your attention to the screen behind me while I dim the lights.
我准备了一段简短的幻灯片,让各位对台湾市场有个了解。现在我把灯光调暗,请大家看我背后的屏幕。

The Taipei area, with a population of six million people, is the trendsetter for Taiwan As you can see from these photos of people in the business area, both men and women are very style-conscious. They have an eye for glamour and good looks, and are willing to spend money to get the look they want. This next slide shows the beauty section of a typical drugstore; lots of famous brands from Japan, Europe, and the United States, plus a broad range of domestic brands. Gentlemen, this is without a doubt a competitive market.
拥有六百万人口的台北地区,是台湾流行趋势的创造者。从这几张商业区人们的照片可以看出,不管男女都很注重打扮,对魅力和美貌的看法也很独特,而且舍得花钱塑造自己想要的外表。下面这张是一家典型购物中心的美容用品区,有数十种来自日本、欧洲和美国的名牌产品,以及台湾本地产品。各位,毫无疑问地,这真是一个竞争激烈的市场!

Moving on to Taipei's hairstyling salons... hmm, that slide seems to have been placed upside down. I'm sorry for the delay; it will only take a moment to flip the slide... There, that's better. As I was saying...
接下来看看台北的发廊,咦,……这张幻灯片似乎放倒了。很抱歉,耽误一下,马上就好……好了,正如我刚才所说的……
Notes:

1. style-conscious 注意打扮的
style有“时尚;风格;品格”之解释,在这里则偏向“流行(样式)”的意思。conscious,有“自觉的”的意思。这两个词合成一个形容词,指“注意流行风尚的”,引申为“着重外表打扮的”。
She was so style-conscious; she read every fashion magazine she could find.
她非常注重打扮,每一本时尚杂志她都看。
2. have an eye for 对……有独到的眼光
Eye的意思并不只有“眼睛”,也可指“眼光;观察力”,在此是指“欣赏事物的能力”,与appreciation同义。这个词组表示某个人对某方面的事物或人有独特而正确的鉴赏能力,能够看出别人所无法察觉的价值。而另一个用法相似的have an ear for意思是“对……(音乐、声音)有独到的鉴赏力”。
When I was young, I had an eye for pretty girls. In fact, I became an expert on the subject.
我年轻的时候,对美丽的女孩有独到的眼光;事实上,我变成了这方面的专家。
3. without a doubt 毫无疑问
Doubt 可当动词、名词使用,意思是“疑问、怀疑”, without则是“没有”,合在一起是副词词组,表示“毫无疑问”。这个词组也可替换成 certainly 或 surely,但语气比这两个词强烈得多。
Without a doubt you have hepatitis--the blood test proves it.
你无疑是患了肝炎--血液检测证实了这一点。

句型总结

●  请观众注意某处
1. Please direct your attention to the screen behind me.
2. Please focus on the screen behind me.
3. Now let's take a look at the screen behind me.
4. I'd like you to look at the screen behind me.
5. Could you please look at the screen behind me?
在开始放幻灯片或投影片等视听辅助器材之前,必须提醒在场的观众,请他们注意。Please direct your attention to...是个直接又常用的说法。

●  说明幻灯片的内容
1. As you can see from these photos of ... both men and women are style-conscious.
2. These photos of ... Indicate that both men and women are style-conscious.
3. These photos of ... Show that both men and women are style-conscious.
4. These photos of ... Prove that both men and women are style-conscious.
5. These photos of ... Highlight the fact that both men and women are style-conscious.
放幻灯片时,应该同时说明幻灯片的内容,帮助观众了解,但只要间歇说明即可,让观众也有时间自己看。
●  为失误道歉
1. I'm sorry for the delay; it will only take a moment to flip the slide.
2. I apologize for the technical difficulties; it will only take a moment to flip the slide.
3. Please pardon the error; it will only take a moment to flip the slide.
4. I'm sorry for the inconvenience; it will only take a moment to flip the slide.
5. I hope you will excuse the delay; it will only take a moment to flip the slide.
Presentation中使用视听辅助器材时,偶尔会发生些小问题,如果那个问题是在场者都察觉得到的,就应立即大方道歉。
特别提示

您相信“一张图胜于千言万语”吧?!事实上,图片不但和人一样能说话,有时还更具有说服力。因此presentation时如果能适时地佐以适当的视听媒体,必能将说明烘托得更精彩。

演讲者的presentation内容若较着重于文字、重点、标题或草图的交代时,较适合选择以白板作为presentation媒体。
在presentation时,演讲者以投影片作为presentation媒体的优点为可以不关灯,在室内正常光线下展示投影片,但无法投射不透明的图片,而且放映的影像常有变形的现象。

 

 

 

一般来说,统计数字可以增加presentation的可信度。Daphne早就准备好以一连串的数字来说服她的观众;她成功的主要因素之一就是能有效地运用各种统计数字与数据。

Most of Taiwan's hair salons are full-service shops that have a regular customer base. Taken as a group, the salons have moderate buying power when it comes to beauty products. The money they spend makes up 13.5% of the total yearly sales for the industry.
台湾大部份的发廊都提供全套的服务,也有固定的客户群。整体看来,发廊在美容用品方面具有相当稳定的购买力,他们购买的费用占了美容用品业年营业额的百分之十三点五。

And the size of this market? Taiwan's twenty million people spent five-point-four billion U.S. dollars on beauty products last year or two hundred and fifty dollars per capita. Salons accounted for four-hundred million of this total. Breaking the figures down even further, five out of every eight dollars were spent by women. One half of the total was spent by people between the ages of nineteen and thirty-five. The last and most surprising statistic shows that teenagers spend the highest per capita total; at three-hundred twenty-three dollars, this is one-point-three times the total of the young adult market. 那么这个市场有多大呢?去年台湾二千万人总共花了五十四亿美元在美容用品上,也就是平均每人消费二百五十美元。而发廊的消费则占了这个总额中的四亿!把这些数字做进一步的分析,我们发现,每八元中有五元是女性销费的;消费总额的二分之一是十九岁到三十五岁年龄层的人消费的。最后一项,同时也是最让人惊讶的统计结果是,十几岁的少年少女每人每年的消费额竟然最高,达到三百二十三没元;是青年消费者的1.3倍。

Notes:

1. make up 组(合)成;造成
make up这个动词词组有相当多的意思,如“弥补、修护、造成”,在本文中则指“造成”,可以用 compose 或 form 来代换。
Teenagers make up 30% of the market in Taiwan.
青少年占了台湾百分之三十的市场。
2. account for 占(花费的)部分
Account 的意思是“计算”。account for 可以做“解释;说明……的原因”,但在此处引申的意思则是“说明金钱是怎么花的;占花费的多少”,譬如每月开销的一半花在房租上,你就可以说:“Rent accounts for half of monthly expenditures.”
Taiwan accounts for 12% of all sales in Asia.
台湾占了整个亚洲地区销售额的百分之十二。
3. breaking (the figures) down 分析;分类
当你将一大堆数字分成小部分来分析,就是将它们break down -“分析;分类”。譬如把生活费区分为食、衣、住、行、医药费等,就是break down the figures on living expenses into food, shelter, education, medical bills, etc.。这个词组也可以用来指“毁损”、“故障”。另外,figures通指经统计、计算而得到的数字。
After breaking the figures down, we found that the company wasn't in such poor financial shape.
分析这些数据之后,我们发现这家公司的财务状况并没那么糟。

句型总结
●  庞大数目的说法
1. 100 百 hundred
2. 1,000 千 thousand
3. 10,000 万 ten thousand
4. 100,000 十万 hundred thousand
5. 1,000,000 百万 million
6. 10,000,000 千万 ten million
7. 100,000,000 亿 hundred million
8. 1,000,000,000 十亿 billion
9. 10,000,000,000 百亿 ten billion
10. 100,000,000,000 千亿 hundred billion
11. 1,000,000,000,000 兆 trillion
12. 1,800 - Eighteen-hundred
13. 90,000 - Ninety thousand
14. 200,000 - Two-hundred thousand
15. 85,600,000 - Eight-five-point-six million
16. 7,000,400,000,000 - Steven-trillion four-hundred million

用英文说庞大的数字时,为避免绕口,有两个简便的办法。第一,当详细数目字不是那么重要时,可使用整数来表达,譬如:two million ninety-three thousand and two(二百零九万三千零二)可以 two million 代替,把零头删除不说。另一种方式是以最大的单位做基准,用小数点的方式来表达,譬如把 five-billion four-hundred million(五十四亿)说成 five-point-four billion,这样的表达方式比较简洁。另外请您注意:billion,美语用法是指“十亿”,而英式英语则为“兆”;trillion,在美语中为“兆”,而英式用法为百万的立方,即为“十万兆”。
●  小数点和百分比
1. 0.32 - O-point-three-two
2. 0.7% -Zero-point-seven percent
3. 89% - Eighty-nine percent
4. 300% - Three-hundred percent
表达数据时,可尽量采用小数点 (decimals) 和百分比 (percentages),听者比较容易掌握。前面提过可以用 point 这个词来简化庞大的数目。但需注意的是,小于1的数目应该在 point 之前加上 zero(零),譬如0.90念成 zero-point-nine;若大于1,就直接读出该数字,然后接 point,例如 two-point-three (2.3)。百分比则大都用来表示比例关系,而且通常只有二位数,所以听者易记易懂。譬如 thirteen-point-five percent (13.5%),书写时数字后一定要加 % 这个符号。
●  分数和比率
1. 5/8 - Five eights
2. 1/6 - A sixth
3. 2/3 - Two thirds
4. 4:5 - Four in five
5. 9:10 - nine out of ten
6. 10:1 - ten to one
7. 1000:1 - a thousand to one
分数和比率的用法在强调数据的对比时相当重要。用英文来表示分数时:分子以数目读出,分母则以序数读出;分子若大于1,分母则须加 s,例如:1/2念成 one second或 one-half,2/3念成 two-thirds,3/4则念成 three-quarters。复杂的分数通常可用关键词 over来表达,如 123/456 要读成:a (one)hundred twenty-three over four hundred fifty-six。谈到比率时,若是要表达"某个数目之中的多少个",有两种表达方式:out of和in,譬如presentation中的five out of eight (5:8),或 one in three (1:3)。而若单纯强调"几比几",则经常用 to。

特别提示

投影片 (overhead transparencies) 虽是最常使用的视听媒体之一,但是一些制作上的基本原则未必人人懂得,因此不合标准的投影片处处可见。不良或字体过小的投影片,彷佛对台下观众实施酷刑;或因为信息过多,使人抓不住重点。以下提供一些投影片制作上的基本原则。

本新闻共2页,当前在第1页  1  2  

 

投影片宜采用横式,勿用直放的格式。这是由于投影机放映台的尺寸,使得直式投影片有截头去尾之虞。
一张投影片,尽量表达一个概念,图样尽量不要超过三个。
运用关键词或标题作提示。整体的文字越短越好,谨记KISS的原则(Keep it simply simple.--简洁扼要),一张投影片勿多过6行字,若为英文则每行不应超过6个词。
投影片投射在屏幕上的文字不宜太小,视与台下观众的距离而定。

 

 

 

图表就像是“数字地图”,可以让看的人一目了然。成功的演说者应该就这些数字、图表加以诠释,让听众即刻掌握这些数字之间的关系。

The statistics I've just given show that Taiwan is definitely a youth-oriented market, and will remain so for another ten years. The companies that successfully target this segment have higher sales and larger market shares year in and year out. I've prepared a few diagrams showing which companies dominate the market, and the types of advertising they use to keep their sales.
我刚才给各位的统计数字说明台湾的确是一个以年轻人为导向的市场,而且未来十年之内仍然会继续是这种状况。因此看准了并打进这个市场区间的企业,每年就能有较高的业绩和市场占有率。我另外还准备了一些图来说明是哪几家公司主宰了目前的市场,以及这些公司运用哪些广告来维持他们的业绩。

This bar chart shows the top four companies and their market shares: the American Jonny&Jammy Company leads the pack with 29%, followed by the domestic firm, V08, at 21%. The Japanese MARUMI is next with 17%, and the French company, La-Rose, is last with 12%. The next chart, a pie graph, shows a breakdown of their advertising: the largest wedge at 62% represents TV commercials; a quarter of their budget goes to magazine and newspaper ads, and the remainder is for purchasing advertising and posters placed in shops where their products are sold.
从这个条形图表可以看出四家最大的公司以及他们的市场占有率。美国的Jonny&Jammy公司以百分之二十九领先群雄,其次是本地的厂家V08,拥有百分之二十一的市场占有率;再下来是日本的MARUMI,拥有百分之十七;法国的La-Rose居末,占了百分之十二的市场。第二个图表是个圆形百分比图,显示这几家公司的各类广告比重。比率最大的那个扇形是电视广告,占百分之六十二;报刊杂志占了百分之二十五,其余的则是促销广告和海报等贴在产品销售卖场的广告。
Notes:

1. youth-oriented 以年轻人为导向的
youth是"青少年人"的意思。基本上各年龄层的泛称有:children(小孩)、teenagers(青少年)、adults(成年人)和the elderly(老年人)。
I suggest youth-oriented commercials when we introduce the new cereal.
我建议推出新的麦片时,打以青少年为导向的电视广告。
2. year in and year out 每年;年复一年
In 和out在这里是指"出"、"入",也就是"来"、"去"的意思。"年来年又去",给人一种连续不断的感觉,有强调的意思。其实它就是指 every year、year after year。
Year in and year out, Ven-Ven is a leader in creative advertising.
年复一年,文文公司仍是创意广告的龙头。
3. lead the pack 领先群雄
Pack 常用的意思是"包;捆",如:a pack of cigarettes(一包香烟);但在这里是指"一群(人)"。整个词组原来是说有一群人在赛跑,其中一人超越了其它竞赛对手,居于领先地位。现在只要是形容某人在团体中表现最好,就可以用这个词组。
Because she studied hard, she led the pack in the race to get into a good university.
她读书很用功,因此考试成绩比其它人好,进了好大学。
句型总结

●  图表的说法与用途
1. What we have here is a bar chart showing...
2. Our monthly sales are shown on this X/Y graph.
3. These three pie graphs each present...
4. The next diagram illustrates...
基本的图表通常有chart、graph 和 diagram。这三个词在意义上原本有细微的差异,但现已互相通用了。图表的种类有很多,最常使用的几种是 bar chart条形图,line chart曲线图(又称 X/Y graph,X/Y坐标图),以及 pie graph圆形百分比图。条形图使人容易看出单一方面的数目,譬如业绩;圆形百分比图则可以看出每一方面占整体的比率。X/Y坐标图能让人清楚地看出两个因素之间的关系,譬如"每月"的"业绩"。每提到一个图表时,要先说出该图表的种类或名称。
●  指出图表某部份
1. The longest bar is...
2. This group of thin wedges shows...
3. The lowest point on the graph is…
解释图表内容时,将每个部份所代表的数字或比率说出来即可。但解释百分比图时,也可加入每个项目彼此或与整体之间的关系,譬如 the largest(最多的……)及 the remainder(其余的……)。另外,在圆形百分比图中的每一部分都可以称为 wedge(扇形)或 slice(片),如 "the smallest wedge / slice shows..."。至于X/Y坐标图就比较复杂,并行线称为 X axis(X轴),垂直线称为 Y axis(Y轴),图上的任何 (X,Y)坐标都是一个 point(一点)。通常要指出曲线上某一点时,最好能明确地指出来,不然就要把X与Y轴所代表的事物明白讲清楚。
●  说明图表的意义
1. This bar chart shows the growth in our sales.
2. Here is a graph showing the growth in our sales.
3. Each bar on the chart represents the growth in our sales.
4. The wedges on this pie graph illustrate the growth in our sales.
5. This diagram focuses on the growth in our sales.
说明图表的意义要简单明了,一句话就能让听众明白。例如文中提到条形图时,立刻表明这张图分别代表the top four companies 和 their market shares(这些称为图表的 factor(s));而看到圆形百分比图时,就表示这张图 "shows a breakdown of their advertising..."。注意,表示整个图所代表的意义用动词 show,而表示图中各部分的要用 represent。

 

 

Daphne提供观众所需的背景数据后,便进入presentation的核心阶段。她必须以专业者的眼光指出天籁在美国成功的销售经验与在台湾所需的行销企划之间所存在的重要差异。

I know EarthSound has used all of these advertising methods successfully in the United States, and you may feel that what works in the States will work in Taiwan. This is definitely not the case. There are important differences between EarthSound's advertising and the advertising of successful companies in Taiwan. 我知道贵公司在美国已经非常成功地运用这几种广告,而且,各位也许会认为,这些广告在美国有效,在台湾应该也一样。其实不然,贵公司的广告与刚才我提到的几家在台湾成功的公司所做的广告有很大的不同。

EarthSound's slogan is, "EarthSound - the healthy alternative for you and the environment," while V08's slogan is, "Thank you, V08, for making me beautiful." Why the difference? My calculations show that 85% of the American public believes they can do something to help the environment, but only 30% of Taiwan's people feel the same. America's politicians and its media have made people aware of this issue. This has created a market for EarthSound's products. If Taiwan's politicians and media would do the same, then the people of Taiwan would be ready for EarthSound's advertising. This hasn't happened yet. 天籁的口号是:"天籁--你和环境的健康选择",V08的口号是:"V08,谢谢你把我变得美丽"。为什么会有这样的差异呢?我的统计显示有85%的美国大众相信他们能做些有助于环境的事,但只有30%的台湾人会这么想。美国的政客和媒体已让大众意识到环境的问题,这就为天籁的产品创造了市场。如果台湾的政客和媒体也能做一样的事,那台湾人才会接受天籁的广告,但这种情况还没有出现。

Notes:

1. (be) aware of 注意;察觉;意识到
aware是个形容词,原意为“知道、晓得”,与of一起使用。be aware of的意思是指感觉到或注意到某一种情况或气氛。同义的词组有become aware of。
We should be aware of the financial risks before committing ourselves.
我们承诺之前应先注意财务上的风险。
2. be ready for... 准备好的;可以(做……)了
ready是形容词,意思为“准备好的”。be ready for即“有准备要做……”,引申有“对……是适合的”之意。presentation中,Daphne认为台湾人的环保意识还不够普遍,故 not ready for accepting(还不能接受)天籁的广告词。
We should be ready for strong competition when we move into the Taiwan market.
开拓台湾市场时,我们应该做好迎接激烈竞争的准备。
3. work 有效;成功;达到目的
work 一般的意思是“工作”(动词;名词);在这里是个口语用法,“有效;发挥功能”的意思,例如它在文中就是用来描述美国的广告词若原封不动搬到台湾就没办法“产生效果”。要注意的是,“work”用做“有效”的意思时,其主词一定是某事、物或某个办法,而不是人。
If your training program works in Hong Kong, it will work in Singapore.
你们的训练课程如果在香港适用,在新加坡也会适用的。

句型总结

●  比较差异
1. There are important differences between (A) and (B).
2. There are great distinctions between (A) and (B).
3. There are obvious contrasts between (A) and (B).
4. We can see discrepancies between (A) and (B).
5. We find great differences between (A) and (B).

presentation中要做一些比较时,最好提醒观众的注意,例如先说“There are important differences between...and...,……(之间)有很大的不同”,然后再说出不同之处。这个句型可以直接指出某两项事物有极大的差异;differences,“差别”在此为复数,表示有多项不同点,并以 important 来形容,有强调差异性的作用;介词 between 之后加相比较的事物,并注意要用 and 来连结这两者。如果差异并不复杂,你甚至可以接着把它们列出来。
●  提出因果关系
2. If (the cause), then (the effect)
3. When (the cause) happens, (the effect) happens
4. When we do (the cause), (the effect) occurs
5. (The cause) caused (the effect)
6. (Effects) were all due to (the cause)

有果必有因,presentation中尤其要说明结果的来由,以服众人。“If..., then...”就是经常用来说明因果的句子。If 后面接表示原因的句子,then 后面接表示结果的句子。通常都是先说明原因再提到结果,这样比较合乎逻辑又易懂。但有时候为了强调,也可以先列出一连串的结果再说明造成的原因。

Saturday, May 19, 2007

(ZT)Microsoft ASP.NET 2.0 Membership API Extended

 

Working with big applications requires extending the Microsoft ASP.NET 2.0 Membership API to handle more detailed member records. In this article, I’ll present one of the available techniques used to extend the Microsoft ASP.NET 2.0 Membership API to solve some of the limitations of that API.

Microsoft ASP.NET 2.0 shipped with a complete membership API that allows developers to manage the application’s users and their roles. However, this API best suits small to medium Web sites due to their limitation in expressing a detailed member record.

"


To have a better understanding of the provider model in ASP.NET 2.0, I highly recommend the following link: Provider Model in Depth (http://msdn.microsoft.com/asp.net/downloads/providers/default.aspx?pull=/library/en-us/dnaspp/html/aspnetprovmod_intro.asp)

"

This article discusses one of the techniques that you can use to overcome this limitation and extend the Microsoft ASP.NET 2.0 Membership API to accommodate custom member records with a solution that works on top of the Membership API without requiring any change in the API.

Article Overview

In the days of ASP.NET 1.x, managing an application’s members and roles was a hectic job, especially when most of the middle and higher-level applications needed that kind of management. You would usually end up creating your own membership API to be able to use in any application you were working on.

Microsoft ASP.NET 2.0 provides many new features, such as the Membership API, where you no longer need to worry about membership management in any application you develop. Microsoft built their Membership API upon the provider model. As with the other new features in ASP.NET 2.0, Microsoft integrated the Membership API into the .NET Framework and you can access all its objects and methods from one namespace reference, System.Web.Security.

The Membership API provides many ready-made features that you had always needed to build in ASP.NET 1.x and that took hundreds of lines of code to accomplish.

For example, the Membership API has the Login control. This control contains the username and password fields used to authenticate every user who tries to access a secure area inside the Web application. In ASP.NET 1.x, you had to add this control to each application you developed. You would end up creating a User control or a Server control so you would not need to repeat your work again and again.

ASP.NET 2.0 provides a Role Management API that works with the Membership API to provide a full solution for the authentication and authorization needed for most Web applications you develop. I will not spend more time on the Membership API controls in this article-you can find many online resources and articles to get more information.

The Membership API works fine with small Web applications. But a problem arises when working with huge applications. For example, the MembershipUser class, which is found in the Membership API and represents a member saved in the application’s database, contains a limited number of properties. This class does not support the First Name and Last Name properties, for example. Usually, in middle to large-scale applications, a member’s record requires the presence of a lot more properties and those properties are not all currently found in the MembershipUser class.

Although the Membership API presents a generic member’s record, Microsoft built the Membership API upon the provider model, so, you can easily solve that limitation and extend the current Membership API to serve your needs.

In addition to the Membership API’s need for more properties, it has another important limitation-by default it only works with Microsoft SQL Server and Active Directory. This last issue is not mainly a limitation just because Microsoft built the Membership API upon the provider model-another provider can easily replace the model with any database implementation available.

Ways to Solve the Problem

You can choose a number of ways to overcome the limitation of the MembershipUser properties in ASP.NET 2.0’s Membership API. This article will focus mainly on extending the default database that ships with the new database-related features in ASP.NET 2.0 so that you can store additional related information about a member in the Web application.

The Membership API provider model consists of the MembershipProvider, which inherits from the ProviderBase, which is the base provider for all the new provider-based features in ASP.NET 2.0. The SqlMembershipProvider and ActiveDirectoryMembershipProvider represent a concrete implementation of the MembershipProvider class.

The Membership class contains a set of static methods that provide the entire functionality of the Membership API to the user-interface layer in any Web application. In addition, the MembershipUser class discussed above represents a single member in the Membership API database. The above can be better understood by having a look at the Membership API class hierarchy (Figure 1).

Click for a larger version of this image.

Figure 1: Membership API class hierarchy.

A typical scenario to overcome the limitation of the Membership API is to develop a new provider that inherits from the MembershipProvider where you override the existing methods and add more functionality as the application requires, but in this article I will show you an entirely different approach. Before I get to my new approach, here is a brief breakdown, through a simple example, of how the scenario mentioned above works so you can see the difference.

In the user-interface layer, an ASP.NET page gathers all the required information about the member using the Profile object to store the additional data. The page calls the static method, CreateUser, which is part of the Membership class. In the new membership provider, the CreateUser method would still function as before by adding the member’s record into the default database; however, it will also be responsible to add the additional member-related data that was saved in the profile object previously, into a new table added to the database that will hold the additional related data on the member’s record.

In this article, I will extend the Membership API in a completely different way. I will show you how to wrap the current Membership API without touching it or even inheriting from it. The basic idea is to create a wrapper over the methods that ship with the Membership API. This way you are extending the set of these methods that affect the data collected to a member’s record. By extending the set, you get a richer environment to work with; the default Membership API methods are still usable in other places where there is no need to create, update, delete, and get a member’s record from a database.

The Concept

The idea behind extending the Membership API is as follows: I’ll create a new ExtendedMembershipUser class that inherits all the default properties from the MembershipUser, and then I’ll add my own custom properties that I will use throughout the article to explain one of the methodologies you can use to extend the ASP.NET 2.0 Membership API. You can later customize this object to fit your needs and requirements.

The new object contains three added properties: FirstName, LastName, and DateOfBirth. This snippet shows a sample of the ExtendedMembershipUser class you’ll learn about in this article:

public ExtendedMembershipUser(MembershipUser _mu,
string FirstName, 
string LastName, 
DateTime DateOfBirth)
: base(_mu.ProviderName, _mu.UserName, 
_mu.ProviderUserKey, 
_mu.Email, _mu.PasswordQuestion, _mu.Comment, 
_mu.IsApproved, _mu.IsLockedOut, _mu.CreationDate,
_mu.LastLoginDate, _mu.LastActivityDate, 
_mu.LastPasswordChangedDate, 
_mu.LastLockoutDate)
 {
// Assign local fields
this._FirstName = FirstName;
this._LastName = LastName;
this._DateOfBirth = DateOfBirth;
 }

As you can see, the ExtendedMembershipUser object inherits from the MembershipUser class. The constructor’s input is an object of type MembershipUser and the three added properties mentioned above. In this case, the constructor makes a call to the base class inheriting from the MembershipUser.

The full constructor shown in the code snippet below has as input a list of all the individual properties of the MembershipUser together with the custom properties that I added.

public ExtendedMembershipUser(string 
ProviderName, string UserName, object 
ProviderUserKey, string Email, string 
PasswordQuestion,
string Comment, bool IsApproved, 
bool IsLockedOut, DateTime CreationDate,
DateTime LastLoginDate, 
DateTime LastActivityDate,
DateTime LastPasswordChangedDate,
DateTime LastLockoutDate,
string FirstName,
string LastName, DateTime DateOfBirth)
: base(ProviderName, 
UserName, ProviderUserKey, Email, 
PasswordQuestion, Comment, IsApproved, 
IsLockedOut, 
CreationDate, LastLoginDate, LastActivityDate,
LastPasswordChangedDate, LastLockoutDate)
 {
// Assign local fields
this._FirstName = FirstName;
this._LastName = LastName;
this._DateOfBirth = DateOfBirth;
}

The next step is to decide what should go in the ExtendedMembership class, which is the provider model’s manager class that should include the static methods available to manage the custom ExtendedMembershipUser. Basically, you need to upgrade the CreateUser, Update, Delete, GetUser, and GetAllUsers methods, which you will mostly use in a Web application. Why are these methods the only methods you need to upgrade?

The answer is simple if you look at all the other methods available in the Membership class:

  • FindUsersByEmail: I doubt this method is widely used. How many times do you allow users to have the same e-mail address in your application? So you don’t need to upgrade this method.
  • FindUsersByName: Do you often allow members in the same application to have the same UserName? Probably not, so once again, you don’t need to upgrade this one.
  • FindUserNameByEmail: You do not need to upgrade this method. You can use it “as is” since you can retrieve the UserName from the Membership API database regardless of what custom properties you add to the database.

So now you know the required methods and the main ExtendedMembershipUser to work with, it is time to start developing this new API.

One important thing to mention before proceeding is that you will build the extended Membership API upon a provider model just as Microsoft built the Membership API.

You’ll use the provider model mainly to build the additional features required to process the new custom properties you added to the ExtendedMembershipUser class. Therefore, the implementation of the methods inside the ExtendedMembership class contains calls to both the default Membership methods (represented by built-in methods like: Membership.GetUser, Membership.CreateUser, etc.) and you’ll create the methods implemented by the new provider model to handle the custom properties added.

After developing the new API, note that you will have a rich environment to work with since you can use both APIs: one to handle the default MembershipUser combined with the custom added properties and one to handle routine functions, such as GetPassword, FindUserNameByEmail, and ValidateUser.

To sum up this section, you will use the new extended Membership to Create, Update, Delete, Get, and GetAll methods now that you have integrated the built-in MembershipUser and the custom properties. For example, adding a new member requires that you add data to the tables that ship with the Membership API and to the table that holds the additional member’s data. You can still use all the other built-in functionalities from the Membership class, for example, the Login control automatically validates a user the same way since you did not change the ValidateUser method.

Set up the Database

This section discusses the table I added to hold the values of the custom properties and the accompanying stored procedures that the new ExtendedMembershipProvider uses.

In this article, I’ll use Microsoft SQL Server 2000. To benefit from the Membership API, you need to create a new empty database, and then install the application services database used by many new database-related features in ASP.NET 2.0, such as the Membership API. To do this, use the following procedure:

  1. Go to Microsoft SQL Server Enterprise Manager and create a new database called ExtendedMemberShipDb.
  2. Type the following text at a command prompt to install the application services database:
{Drive Letter}:\Windows\Microsoft.NET\
Framework\{Version Number}\aspnet_regsql.exe

Use the aspnet_regsql.exe utility to install the application service’s tables into the current database. Once the executable runs, install the required tables, stored procedures, and user-defined functions to the ExtendedMemberShipDb database created above.

For more information on how to do the above process, you can check this step-by-step post on my blog: “Install Application Services Database on Microsoft SQL Server 2000” (http://bhaidar.net/cs/archive/2006/01/22/install-application-services-database-on-microsoft-sql-server-2000.aspx).

Now that you have the database installed, create a new table (called UserInfo) to hold the values for the custom properties that you added.

This table contains the following columns:

  • UserId of type UniqueIdentifier and designated as the primary key of the UserInfo table. It is also the primary key of this table.
  • FirstName of type VarChar and size 50
  • LastName of type VarChar and size 50
  • DateOfBirth of type DateTime

This table stores the extended data related to the member in the application. Note that you used the UserId as a primary key, which is of type UniqueIdentifier. UserId is the same key used as a primary key for the member’s record in the default Membership API tables. By using the same key, you can relate the UserInfo table to all the other member management tables.

Now it is time to create the stored procedures that you will use in the SqlMembershipProvider (you will implement SqlMembershipProvider later in this article).

The stored procedures are:

  • aspnet_ExtendedMemberShip_CreateUser
  • aspnet_ExtendedMembership_DeleteUser
  • aspnet_ExtendedMembership_GetAllUsers
  • aspnet_ExtendedMembership_GetUserByUserId
  • aspnet_ExtendedMembership_GetUsersByUserIds
  • aspnet_ExtendedMembership_UpdateUser

The CreateUser stored procedure (Listing 1) checks whether the member to be added is already found in the table-if not found, CreateUser will insert the record.

The same logic follows in the DeleteUser stored procedure (Listing 2); that is, if the record is already in the table, delete it.

I will not cover the entire stored procedures here since they all follow the same logic shown above. You can look at all of those stored procedures in the downloadable code accompanying this article.

Now that you have created the table and stored procedures, I’ll show you how to develop the ExtendedMembershipProvider.

ExtendedMembershipProvider

The Extended Membership API consists of the following:

  • The ExtendedMembershipUser class, which represents a single user record with the default and custom properties of a member.
  • The ExtendedMembership class containing the static methods accessible by the user interface layer.
  • All the files that constitute the Extended Provider, which is used to provide concrete implementation for the custom methods added in the ExtendedMembership class.

The ExtendedMembership class is similar to the Membership class in the ASP.NET 2.0 Membership API. The static methods contained in the ExtendedMembership class combine functionalities from both the default Membership class and added implemented methods in the ExtendedMembershipProvider used to handle the custom data. I’ll go through each implemented method and explain how it works.

  • CreateUser: Used to add a new member to the database.
  • DeleteUser: Used to delete a member stored in the database.
  • Update: Used to update a member’s record in the database.
  • GetUser: Used to retrieve a single user stored in the database
  • GetAllUsers: Used to retrieve a list of users stored in the database.

You will use the above methods to manage both the default and custom properties of a member’s record. This idea will be clear once I discuss the above methods in detail.

The CreateUser method has two overloaded methods: the first uses a CreateUserWizard ASP.NET control to create the user and the second creates a user programmatically using the ExtendedMembership API. In the code below, you can see an example of the method version that you can use inside the CreateUser event that ASP.NET will fire after it creates the user through the implicit call to the Membership API inside the CreateUserWizard:

public static bool 
CreateUser(string UserName, string FirstName, 
string LastName, DateTime DateOfBirth)
{
// Get the UserId
object UserId =
 Membership.GetUser(UserName).ProviderUserKey;
// Call the custom provider
return MemberShip.Provider.CreateUser(
new UserInfo(UserId, FirstName, LastName, 
DateOfBirth));
}

This method takes as input the UserName, FirstName, LastName, and DateOfBirth. It is useful when working with the CreateUserWizard. Later in the section I will present some examples on how to use the Extended Membership Provider where you will see that I add the custom fields in the CreateUserWizard; however, to add the user data into the default tables used by Membership API, I have to let the CreateUserWizard call the CreateUser method in the default Membership provider. Then I have to manually add the details of that user to the custom table using the above method.

The method starts by using the Membership.GetUser method to insert the UserId. Then you’ll call the method from the provider that you will build soon. ASP.NET calls the method CreateUser, which takes as input a parameter of type UserInfo class. This method is an internal object that the Extended Membership Provider uses to facilitate passing data and ASP.NET adds the custom properties to it as details for the default member.

The other version of this method takes as input all the properties of a MembershipUser, in addition to the custom properties you added. Use this method when you are creating the user programmatically and not through the CreateUserWizard (Listing 3).

The method creates the member’s record with the default properties using the Membership.CreateUser method. If the creation was successful, it calls the method inside the ExtendedMembershipProvider that adds the custom properties to the UserInfo table. If that method inserts the record successfully, it returns a new instance of the ExtendedMembershipUser object; otherwise, it deletes the record and returns a value of null.

You can see the DeleteUser method in the following code snippet:

public static bool DeleteUser(string UserName)
{
// A simple call to the default 
// DeleteUser method specifying that
// all related data are to be deleted
// Get the UserId to delete
object UserId = 
Membership.GetUser(UserName).ProviderUserKey;
if (UserId != null)
   {
if (Membership.DeleteUser(UserName, true) 
      == true)
      {
// You can delete from your 
// custom table
// Execute your custom method
// to delete from your custom
// table
return MemberShip.Provider.Delete(UserId);
      }
}
return false;
}

The above method accepts as input the UserName. DeleteUser first tries to delete the member’s record from the Membership API default database; if it deletes the record successfully, it deletes the details of that record from the UserInfo table.

The way the Update method (Listing 4) works is as follows. It assumes that ASP.NET only allows you to update the Comment, Email, IsApproved, and DateOfBirth properties for a member’s record, which is exactly what happens in the Membership API.

Instead of directly updating the record, it checks if the above listed properties of the input ExtendedMembershipUser parameter are different from those stored for that member in the database. ASP.NET uses a flag for that process-if the flag is changed, at least one of the fields of the member’s record stored in the database has changed and needs to be updated. Based on that flag, the Update method would update the member’s record in both the default database and the UserInfo table.

The GetUser method has several overloads (Listing 5 shows the main overload). The method first tries to retrieve the member’s record from the Membership API database. If it finds the record, it uses the ProviderUserKey, which is the primary key in the UserInfo table, to retrieve the detail record from the UserInfo table.

The GetAllUsers method has two overloads; Listing 6 shows the main one.

The GetAllUsers method gets a list of all member records from the default Membership database. It then creates a comma-delimited list of all member IDs and passes the list to the ExtendedMembershipProvider method to get all records whose IDs are present in the list. After that, GetAllUsers joins the data for each record from both data sources and returns a collection of ExtendedMembershipUser object.

The above explanation dealt with the ExtendedMembership class, which contains all the static methods available for the new extended Membership API. In provider model terms, the ExtendedMembership class is the Manager class. The Manager class and the following classes together form the complete ExtendedMembershipProvider:

  • ExtendedMembershipProvider
  • ExtendedMembershipProviderCollection
  • ExtendedMembershipConfiguration
  • ExtendedSqlMembershipProvider
  • ExtendedMembership

The above five classes constitute the major elements of a provider model in ASP.NET 2.0. Since I am discussing the extended membership provider, there is a very good link for a set of articles on the different providers available in ASP.NET 2.0, and a Provider Toolkit that creates for you the above five classes. It mainly creates the needed skeleton for a provider and you just add your functionality to those classes using your own methods. One note about the Provider Toolkit is that you need to configure the classes included with the provider name, namespace, and the type of the provider you want Oracle, Microsoft Access, MySql, or Microsoft SQL Server to use. This configuration should take you approximately 30-60 minutes to do.

To save you time, I created a small Windows application utility (Provider ToolKit), which you can download at (http://bhaidar.net/cs/archive/2006/01/23/55.aspx). I’ve explained this utility on that site, and the Provider ToolKit can really make your life easy in developing providers for your own features in ASP.NET 2.0. Moreover, I used the toolkit to create the above classes of the ExtendedMembershipProvider.

The MembershipProvider inherits from the ProviderBase, which is the base class for all providers in ASP.NET 2.0, and contains the abstract methods listed in Table 1:

  • The ExtendedSqlMembershipProvider inherits from the ExtendedMembershipProvider and implements the above methods, which the ExtendedMembership class accesses, as shown in the code snippet below:
public class ExtendedSqlMembershipProvider : 
ExtendedMembershipProvider
{

The implementation follows the same technique used by the default Membership Provider API. I will just show one sample of those methods; Listing 7 shows the complete SqlMemeberShipProvider .I will discuss the CreateUser method located inside the SqlMemberShipProvider (Listing 7).

The CreateUser method takes as input a UserInfo instance, does some checking on the validity of the input parameter, and then uses the normal ADO.NET code to add the new member record to the UserInfo table. The other methods in the class follow the same steps in their implementation.

You will not have to touch the other classes that are part of the ExtendedMembershipProvider and that the utility above created. ASP.NET uses them to manage the configuration section of the new provider in the Web.config file.

Test the Extended MemberShip API

Now that you have developed the ExtendedMemberShipProvider, I’ll look at a set of examples on how to use this provider.

Before you can take the next step, you need to configure the newly developed provider in the Web.config file of the Web application. The Provider ToolKit utility creates the configuration sections needed to add to the Web.config file (Listing 8).

The Web.config file configures the current Web application to use the new ExtendedMemberShipProvider.

Now that you have configured the new provider in the Web.config file, you can start testing that provider. All the samples that I’ve presented are part of a Web application in the accompanying downloadable code of this article.

The first sample creates a new user (see the ASPX page in Figure 2 below). Figure 2 shows the CreateUserWizard that you customized to have three additional fields: FirstName, LastName, and DateOfBirth. By default, the CreateUserWizard calls the CreateUser in the Membership Provider; however, since I’m using a different provider, I will keep the default behavior of that control the same, and then I can override the event called OnCreatedUser, where I’ll add my custom fields to the table UserInfo.

Click for a larger version of this image.

Figure 2: This figure shows the CreateUserWizard with three custom fields.

Once the CreateUserWizard finishes adding the member records to the Membership-related tables, ASP.NET raises the OnCreatedUser event to process the custom fields. Listing 9 shows the implementation of that event.

The CreateUserWizard1_CreatedUser method gets the custom-field values from the CreateUserWizard control, checks if ASP.NET actually created the member records in the database, and then adds the detailed information about that member to the UserInfo table. The second sample deletes a user from the database.

Figure 3 below shows a drop-down list with all the user names found in the database. When you select a user name, you need to press the Delete button to completely delete the member record and its details from the UserInfo table.

Click for a larger version of this image.

Figure 3:This figure shows the Delete User screen in the sample application.

I’ve implemented the event handler for the Delete button in the following code snippet:

protected void btnDelete_Click(object sender,
   EventArgs e)
{
// Get the selected user name:
string UserName = 
this.ddlUserNames.SelectedValue.ToString();
// Delete 
if (ExtendedMembership.DeleteUser(UserName) 
    == true)
     {
this.lblMsg.Text = "User deleted"; 
return;
    }
this.lblMsg.Text = "User could not be deleted!";
}

The last sample that I’ll discuss is the Update User screen (Figure 4). The UI displays the member’s user name, Bilal, from the drop-down list. Once you select the user name, you need to click the Submit button. After that ASP.NET displays a populated form with the chosen member’s information below the drop-down list. You can now edit those fields and click “Update User” to update that user in the database.

Click for a larger version of this image.

Figure 4: This figure shows the Update User screen.

I’ve implemented the event handler for the Update User button (Listing 10). The method above gets the field values from the form, and then calls the ExtendedMembership’s Update method to update the default values stored and the ones stored in the UserInfo table.

There are still two methods that I have not mentioned here: ExtendedMembership.GetUser and ExtendedMembership.GetAllUsers. For instance, I used the ExtendedMembership.GetAllUsers method above when I loaded the UserName drop-down list. I used the ExtendedMembership.GetUser method above to retrieve and display all the details of a member record when I selected a user name.

I suggest you look at all the above code samples in the downloadable code accompanying this article.

Advantages and Disadvantages of the Extended Membership API

As mentioned at the beginning of this article, there are two ways to extend the Membership API in ASP.NET 2.0. One extension inherits from the MembershipProvider and thus overrides all the methods that the MembershipProvider provides using custom code to extend the Membership API. I discussed the second extension throughout this article where instead of touching the default Membership API you built upon it-gaining the chance to use both the Extended Membership API and the default one in the same Web application.

From a performance point of view, the latter method inherits from MembershipProvider, and then uses other new features in ASP.NET 2.0 to process any customized data. For example, using the first method you will have to override the CreateUser method to add member records to the default Membership API database and to the UserInfo table. In that case, you must save all the custom data from the CreateUserWizard in the Profile object (another new feature in ASP.NET 2.0) in the user-interface layer. The Profile object allows you to process custom data inside the overridden CreateUser method-thus, utilizing more than one feature for the sake of extending the Membership API.

In this article I created a provider that works as a wrapper around the default MembershipProvider without touching any of its methods; instead, I used the default methods provided by the Membership provider like CreateUser, DeleteUser, etc.

In the latter method, if you want to use all the available methods in the Membership API, you have to override all the built-in methods that ship with the MembershipProvider; while in the second method, creating the wrapper, you are extending the set of those methods that affect the data collected to a member’s record. By extending the set, you get a richer environment to work with; the default Membership API methods are still usable in other places where there is no need to create, update, delete, and get a member’s record from a database. For instance, you can still use the ValidateUser method, which is automatically called by the Login control from the default Membership API, since it does nothing but check if the user name and password that the user entered are correct and valid in the default database. This method was not extended by the new provider since it can function well without the need to have any information about the new customized data added. By doing this, you are leaving most of the Membership controls to function normally. However, in the first way of extending the Membership API, you have to override the ValidateUser method if you are going to validate a user and it is something you would normally do!

Finally, the techniques I’ve discussed in this article give you the chance to use the default Membership API in addition to a set of customized methods through the Extended Membership API. The first method lacks that rich environment and requires more work to implement all built-in methods in the Membership API, if you need to utilize them, and uses the Profile object in ASP.NET 2.0 to accomplish the task.

Conclusion

To wrap things up, I have discussed one of the methods used in ASP.NET 2.0 to extend the Membership API. I’ve demonstrated extended functionality to the Membership API and left the main Membership API untouched; thus, giving you a richer environment to work with.

In addition to the main focus on extending the Membership API, I have introduced the Provider ToolKit, which you can download for free, to help you build your own provider models and presented a utility that you can use to configure and customize the Provider ToolKit to your needs in a matter of few seconds.

Last but not least, I would like to thank Peter Kellner who inspired me to write this article, Alister Jones who never stops supporting me, to the LebDev.NET user group, and to all my professors and friends. Finally, I would like to dedicate this article to the memory of my late, great friend, Jim Ross, who believed in me and never missed a chance to push me forward.

Bilal Haidar Fast Facts

The Membership API is built on the provider model so you can extend it.


How to Install the Application Services Database

Open a Command prompt window, and then go to the following location:

{Drive Letter}:\Windows

\Microsoft.NET\Framework\

{Version Number}\

aspnet_regsql.exe


Provider ToolKit

You can download the Provider Toolkit from the MSDN Web site here (http://msdn2.microsoft.com/en-us/asp.net/aa336558.aspx).


Table 1: Abstract methods of the ProvideBase.

Method
Return Type
Syntax

CreateUser
bool
CreateUser(UserInfo userInfo);

Delete
bool
Delete(object UserId);

UpdateUser
void
UpdateUser(object UserId, DateTime DateOfBith);

GetUser
UserInfo
GetUser(object UserId);

GetAllUsers
List<UserInfo>
GetAllUsers();

GetUsersByUserIds
List<UserInfo>
GetUsersByUserIds(string UserIds);

Listing1: CreateUser stored procedure

CREATE PROCEDURE aspnet_ExtendedMembership_CreateUser
(
   @UserId UNIQUEIDENTIFIER,
   @FirstName VARCHAR(50),
   @LastName VARCHAR(50),
   @DateOfBirth DATETIME
)
AS
BEGIN
-- @ErrorCode to handle problems during adding a new record
DECLARE @ErrorCode INT
SELECT @ErrorCode = 0
IF( EXISTS( SELECT UserId FROM 
    dbo.aspnet_ExtendedMembership_UserInfo
      WHERE @UserId = UserId ) )
     GOTO Cleanup
INSERT INTO dbo.aspnet_ExtendedMembership_UserInfo
  (UserId, FirstName, LastName, DateOfBirth)
VALUES   
  (@UserId, @FirstName, @LastName, @DateOfBirth)
SELECT @ErrorCode = @@ERROR
IF( @ErrorCode <> 0 )
   GOTO Cleanup
RETURN 0
Cleanup:
   SELECT @ErrorCode = -1
   RETURN @ErrorCode
END
GO

Listing 2: DeleteUser stored procedure

CREATE PROCEDURE aspnet_ExtendedMembership_DeleteUser
(
   @UserId UNIQUEIDENTIFIER,
   @NumTablesDeletedFrom INT OUTPUT
)
AS
BEGIN
-- @ErrorCode to handle problems during deleting a record
DECLARE @ErrorCode INT
DECLARE @RowCount INT
SELECT @ErrorCode = 0
SELECT @RowCount = 0
SELECT @NumTablesDeletedFrom = 0
IF (@UserId IS NULL)
   GOTO Cleanup
IF (EXISTS (SELECT UserId
        FROM dbo.aspnet_ExtendedMembership_UserInfo
        WHERE @UserId = UserId))
BEGIN
DELETE 
  FROM aspnet_ExtendedMembership_UserInfo
  WHERE UserId = @UserId
SELECT @ErrorCode = @@ERROR, @RowCount = @@ROWCOUNT
IF( @ErrorCode <> 0 )
   GOTO Cleanup
   IF (@RowCount <> 0)
        SELECT   @NumTablesDeletedFrom = @NumTablesDeletedFrom + 1
   END
   ELSE
     GOTO Cleanup
   RETURN 0
Cleanup:
   SELECT @NumTablesDeletedFrom = 0
   SELECT @ErrorCode = -1
   RETURN @ErrorCode
END
GO

Listing 3: Second overloaded method of CreateUser

public static ExtendedMembershipUser 
CreateUser(string UserName, string Password, 
string Email, string PasswordQuestion,
string PasswordAnswer, 
bool IsApproved, object ProviderUserKey, 
string FirstName, string LastName, 
DateTime DateOfBirth, out MembershipCreateStatus status)
{
// Call the default Membership CreateUser method
MembershipUser _mu = 
  Membership.CreateUser(UserName, Password, Email, 
  PasswordQuestion, PasswordAnswer, IsApproved, out status);
if (status != MembershipCreateStatus.Success)
{
return null;
}
// Now since the CreateUser was successful, add
// the additional data to your custom table
UserInfo _ui = 
new UserInfo(_mu.ProviderUserKey, FirstName, LastName,
   DateOfBirth);
if (ExtendedMembership.Provider.CreateUser(_ui) == true)
return new ExtendedMembershipUser(_mu,
     FirstName, LastName, DateOfBirth);
// Since the record was not created in the custom table
// you also need to remove it from the default
// membership database
ExtendedMembership.DeleteUser(UserName);
return null;
}

Listing 4: UpdateUser method

static public void Update(ExtendedMembershipUser msu)
{
// Flags used to check whether it is necessary to
// update the user
// if no changes are there or just update the user
// just to preserve a database call
bool IsDefaultDirty = false;
bool IsCustomDirty = false;
// Get the user using your local GetUser method
ExtendedMembershipUser _msu =
  ExtendedMembership.GetUser(msu.UserName);
// Check default properties
if (_msu.Comment == null || _msu.Comment.CompareTo(msu.Comment)
  != 0)
{
IsDefaultDirty = true;
_msu.Comment = msu.Comment;
}
if (_msu.Email == null || _msu.Email.CompareTo(msu.Email) != 0)
{
IsDefaultDirty = true;
_msu.Email = msu.Email;
}
if (_msu.IsApproved != msu.IsApproved)
{
IsDefaultDirty = true;
_msu.IsApproved = msu.IsApproved;
}
if (_msu.DateOfBirth != msu.DateOfBirth)
{
IsCustomDirty = true;
_msu.DateOfBirth = msu.DateOfBirth;
}
// If there are any changes in the default
// MembershipUser, update it
MembershipUser _mu = null;
if (IsDefaultDirty == true)
{
// Get the default MembershipUser from the
// ExtendedMembershipUser object
_mu = new MembershipUser(_msu.ProviderName, _msu.UserName,
  _msu.ProviderUserKey, _msu.Email,
_msu.PasswordQuestion,
_msu.Comment, _msu.IsApproved,
_msu.IsLockedOut, _msu.CreationDate, _msu.LastLoginDate, 
_msu.LastActivityDate,
_msu.LastPasswordChangedDate, _msu.LastLockoutDate);
// Call default Membership Update method
Membership.UpdateUser(_mu);
}
// Check if the custom data needs to be updated
if (IsCustomDirty == true)
{
// Update your custom data
  ExtendedMembership.Provider.UpdateUser(
  _mu.ProviderUserKey,_msu.DateOfBirth);
            }
        }

Listing 5: GetUser method

public static ExtendedMembershipUser 
  GetUser(string UserName, bool UserIsOnline)
  {
// Get the user from the default membership
  MembershipUser _mu = Membership.GetUser(UserName,UserIsOnline);
// Check that the returned user is not null
if (_mu != null)
{
// Get the UserId 
object UserId = _mu.ProviderUserKey;
// Get the additional related data from the
// custom database
UserInfo _ui = ExtendedMembership.Provider.GetUser(UserId);
if (_ui != null)
{
// Combine both objects to get a full one
return new ExtendedMembershipUser(_mu, _ui.FirstName,
_ui.LastName, _ui.DateOfBirth);
}
// The returned user object from the custom
// database is null
return null;
}
// The returned user object from the membership
// database is null
return null;
        }

Listing 6: GetAllUsers main method

public static List<ExtendedMembershipUser> 
  GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
  {
// Get all users from membership database
MembershipUserCollection _muc = 
 Membership.GetAllUsers(pageIndex, pageSize, out totalRecords);
if (_muc != null)
            {
// Single ExtendedMembershipUser instance
ExtendedMembershipUser _msu = new ExtendedMembershipUser();
// StringBuilder to hold all user IDs
StringBuilder sbUserIds = new StringBuilder();
// Convert the MembershipCollection to a list
List<MembershipUser> _muList = new List<MembershipUser>();
foreach (MembershipUser _mu1 in _muc)
{
_muList.Add(_mu1);
sbUserIds.AppendFormat("'{0}',", _mu1.ProviderUserKey);
}
// Get all users from both membership and
// custom database
List<ExtendedMembershipUser> _msuList = new 
List<ExtendedMembershipUser>();
// If there are elements
if (sbUserIds.Length > 0)
{
// Calling method GetAllUsers with a list of
// user IDs as an input
List<UserInfo> _uiList = new List<UserInfo>();
_uiList = ExtendedMembership.Provider.GetUsersByUserIds(
  sbUserIds.ToString().Remove(sbUserIds.Length - 1));
if ((_uiList != null) && (_uiList.Count == _muc.Count))
{
// Loop though both lists to join them
int counter = _muc.Count;
// MembershipUser List
for (int i = 0; i < counter; i++)
   {
// UserInfo List
for(int j=0; j<counter;j++)
  {
if (_uiList[j].UserId.ToString().CompareTo(
  _muList[i].ProviderUserKey.ToString()) == 0)
_msuList.Add(new ExtendedMembershipUser(_muList[i], 
_uiList[j].FirstName, _uiList[j].LastName, 
_uiList[j].DateOfBirth));
}
}
if (_msuList.Count > 0)
return _msuList;
 }
 }
 }
return null;
 }

Listing 7: SqlMemberShipProvider class

public class ExtendedSqlMembershipProvider : 
ExtendedMembershipProvider
{
#region Implement all the methods in the MemberShipProvider
internal override bool CreateUser(UserInfo userInfo)
{
// Do some validation on the parameters
if ((userInfo == null) || (userInfo.UserId == null))
// Failed, no user details creation
return false;
if (string.IsNullOrEmpty(userInfo.FirstName) || 
  (string.IsNullOrEmpty(userInfo.LastName)) || 
  (userInfo.DateOfBirth < DateTime.MinValue))
// Failed, no user details creation
   return false;
// SQL Connection
SqlConnection con = null;
try
{
con = new SqlConnection(this.connectionString);
SqlCommand command1 = new 
SqlCommand("dbo.aspnet_ExtendedMembership_CreateUser", con);
                    command1.CommandType = 
CommandType.StoredProcedure;
      command1.Parameters.Add(this.CreateInputParam("@UserId", 
SqlDbType.UniqueIdentifier, userInfo.UserId));
      command1.Parameters.Add(this.CreateInputParam("@FirstName",
SqlDbType.VarChar, userInfo.FirstName));
      command1.Parameters.Add(this.CreateInputParam("@LastName", 
SqlDbType.VarChar, userInfo.LastName));
      command1.Parameters.Add(this.CreateInputParam("@DateOfBirth",
SqlDbType.DateTime, userInfo.DateOfBirth));
SqlParameter parameter1 = new 
SqlParameter("@ReturnValue", SqlDbType.Int);
                    parameter1.Direction = 
ParameterDirection.ReturnValue;
                    command1.Parameters.Add(parameter1);
// Execute the command
con.Open();
command1.ExecuteNonQuery();
int result = (parameter1.Value != null) ? 
  ((int)parameter1.Value) : -1;
  return (result == 0);
   }
                catch
                {
   throw new ApplicationException("Exception adding user");
                }
                finally
                {
                    if (con != null)
                    {
                        con.Close();
                        con = null;
                    }
                }
        }
        /// <summary>
        /// This method deletes a user from the custom table
        /// </summary>
        /// <param name="UserId"></param>
        internal override bool Delete(object UserId)
        {
// Do some validation on the parameters
        if (UserId == null)
         throw new ApplicationException("UserId cannot be null");
// SQL Connection
            SqlConnection con = null;
            try
            {
                con = new SqlConnection(this.connectionString);
             SqlCommand command1 = new 
SqlCommand("dbo.aspnet_ExtendedMembership_DeleteUser",
   con);
    command1.CommandType = CommandType.StoredProcedure;
    command1.Parameters.Add(
    this.CreateInputParam("@UserId", 
    SqlDbType.UniqueIdentifier, UserId));
    SqlParameter parameter1 = new   
      SqlParameter("@NumTablesDeletedFrom",
       SqlDbType.Int);
       parameter1.Direction = ParameterDirection.Output;
       command1.Parameters.Add(parameter1);
// Execute the command
       con.Open();
       command1.ExecuteNonQuery();
       int num1 = (parameter1.Value != null) ?
         ((int)parameter1.Value) : -1;
          return (num1 > 0);
          }
          catch
          {
        throw new ApplicationException("Delete user exception ");
          }
           finally
          {
           if (con != null)
             {
             con.Close();
             con = null;
                }
            }
        }
        /// <param name="DateOfBith"></param>
       internal override void UpdateUser(object UserId,
         DateTime DateOfBith)
        {
// Do some validation on the parameters
        if (UserId == null)
          throw new ApplicationException("UserId cannot be null");
        if (DateOfBith <= DateTime.MinValue)
         throw new ApplicationException("Invalid date of birth ");
// SQL Connection
SqlConnection con = null;
try
{
con = new SqlConnection(this.connectionString);
SqlCommand command1 = new 
  SqlCommand("dbo.aspnet_ExtendedMembership_UpdateUser", con);
         command1.CommandType = CommandType.StoredProcedure;
         command1.Parameters.Add(this.CreateInputParam("@UserId", 
SqlDbType.UniqueIdentifier, UserId));
     command1.Parameters.Add(this.CreateInputParam("@DateOfBirth",
SqlDbType.DateTime, DateOfBith));
SqlParameter parameter1 = 
   new SqlParameter("@ReturnValue", SqlDbType.Int);
   parameter1.Direction = ParameterDirection.ReturnValue;
   command1.Parameters.Add(parameter1);
// Execute the command
con.Open();
command1.ExecuteNonQuery();
int result = (parameter1.Value != null) ? 
  ((int)parameter1.Value) : -1;
if (result != 0)
{
// Something went wrong with the execution
// Update didn't work properly
}
}
catch
{
  throw new ApplicationException("Exception in updating user");
 }
 finally
      {
     if (con != null)
       {
       con.Close();
       con = null;
       }
       }
        }
internal override UserInfo GetUser(object UserId)
{
// SQL Connection
SqlConnection con = null;
// Define Data reader
SqlDataReader reader1 = null;
try
{
  con = new SqlConnection(this.connectionString);
  SqlCommand command1 = new     
  SqlCommand("dbo.aspnet_ExtendedMembership_GetUserByUserId",
  con);
command1.CommandType = CommandType.StoredProcedure;
         command1.Parameters.Add(this.CreateInputParam("@UserId", 
SqlDbType.UniqueIdentifier, UserId));
                SqlParameter parameter1 = new 
SqlParameter("@ReturnValue", SqlDbType.Int);
parameter1.Direction = ParameterDirection.ReturnValue;
command1.Parameters.Add(parameter1);
// Execute the Reader
con.Open();
reader1 = command1.ExecuteReader(CommandBehavior.CloseConnection);
if (reader1.Read())
{
  string text1 = reader1.GetString(0);
  string text2 = reader1.GetString(1);
  DateTime time1 = reader1.GetDateTime(2);
  return new UserInfo(UserId,text1,text2,time1);
}
return null;
}
catch
{
throw new ApplicationException("Exception in GetUser");
}
finally
{
if (reader1 != null)
  {
  reader1.Close();
  reader1 = null;
  }
if (con != null)
  {
  con.Close();
  con = null;
  }
}
return null;
}
internal override List<UserInfo> GetAllUsers()
List<UserInfo> collection1 = new List<UserInfo>();
// SQL Connection
SqlConnection con = new SqlConnection(this.connectionString);
// Define the data reader
SqlDataReader reader1 = null;
try
{
con = new SqlConnection(this.connectionString);
SqlCommand command1 = new SqlCommand("dbo.aspnet_ExtendedMembership_GetAllUsers",
  con);
  command1.CommandType = CommandType.StoredProcedure;
// Open connection
con.Open();
reader1 = command1.ExecuteReader(CommandBehavior.CloseConnection);
while (reader1.Read())
{
  Guid guid1 = reader1.GetGuid(0);
  string text1 = reader1.GetString(1);
  string text2 = reader1.GetString(2);
DateTime time1 = reader1.GetDateTime(3);
// Add to collection
collection1.Add(new UserInfo(guid1,text1,text2,time1));
}
return collection1;
            }
            catch
           {
           throw new ApplicationException("GetAllUsers");
            }
            finally
            {
                if (reader1 != null)
                {
                    reader1.Close();
                    reader1 = null;
                }
                if (con != null)
                {
                    con.Close();
                    con = null;
                }
            }
        }
 internal override List<UserInfo> GetUsersByUserIds(string UserIds)
        {
// Do some validation on the parameters
            if (string.IsNullOrEmpty(UserIds))
                return null;
   List<UserInfo> collection1 = new List<UserInfo>();
// SQL Connection
SqlConnection con = new 
SqlConnection(this.connectionString);
// Define the data reader
SqlDataReader reader1 = null;
try
{
con = new SqlConnection(this.connectionString);
SqlCommand command1 = 
new SqlCommand("dbo.aspnet_ExtendedMembership_GetUsersByUserIds",
con);
command1.CommandType = CommandType.StoredProcedure;
   command1.Parameters.Add(this.CreateInputParam("@ListOfUserIds",
SqlDbType.VarChar, UserIds));
// Open connection
con.Open();
 reader1 = command1.ExecuteReader(CommandBehavior.CloseConnection);
while (reader1.Read())
{
Guid guid1 = reader1.GetGuid(0);
string text1 = reader1.GetString(1);
string text2 = reader1.GetString(2);
DateTime time1 = reader1.GetDateTime(3);
// Add to collection
collection1.Add(new UserInfo(guid1, text1, text2, time1));
                }
                return collection1;
            }
            catch
            {
// throw new ApplicationException
// ("Exception in GetUsersByUserIds");
            }
            finally
            {
                if (reader1 != null)
                {
                    reader1.Close();
                    reader1 = null;
                }
                if (con != null)
                {
                    con.Close();
                    con = null;
                }
            }
            return null;
        }
        #endregion  
#region Helper Methods
private SqlParameter CreateInputParam(string paramName,
  SqlDbType dbType, object objValue)
{
SqlParameter parameter1 = new SqlParameter(paramName, dbType);
            if (objValue == null)
            {
                parameter1.IsNullable = true;
                parameter1.Value = DBNull.Value;
                return parameter1;
            }
            parameter1.Value = objValue;
            return parameter1;
        }
        #endregion
        #region Provider Section
private string connectionString;
public override void Initialize(string name, 
System.Collections.Specialized.NameValueCollection config)
      {
if ((config == null) || (config.Count == 0))
throw new ArgumentNullException("...");
if (string.IsNullOrEmpty(config["description"]))
{
 config.Remove("description");
 config.Add("description", "Put a localized description here.");
}
// Let ProviderBase perform the basic
// initialization
base.Initialize(name, config);
// Perform feature-specific provider
// initialization here
// Get the connection string
string connectionStringName = config["connectionStringName"];
if (String.IsNullOrEmpty(connectionStringName))
   throw new ProviderException("You must specify a 
                                connectionStringName attribute.");
ConnectionStringsSection cs = 
(ConnectionStringsSection)ConfigurationManager.GetSection
("connectionStrings");
if (cs == null)
   throw new ProviderException("An error occurred retrieving the
                                connection strings section.");
if (cs.ConnectionStrings[connectionStringName] == null)
throw new ProviderException("The connection string could not be 
                       found in the connection strings section.");
else
connectionString = 
cs.ConnectionStrings[connectionStringName].ConnectionString;
if (String.IsNullOrEmpty(connectionString))
   throw new ProviderException("Connection string is invalid.");
   config.Remove("connectionStringName");
// Check to see if unexpected attributes were
// set in configuration
if (config.Count > 0)
{
string extraAttribute = config.GetKey(0);
if (!String.IsNullOrEmpty(extraAttribute))
 throw new ProviderException("…");
else
throw new ProviderException(". . .");
   }
}
        #endregion
    }

Listing 8: Web.config file

<?xml version="1.0"?>
<configuration>
<configSections>
<section name="MemberShip" 
type="ExtendedMemberShip.ExtendedMembershipConfiguration, 
         ExtendedMemberShip" 
allowDefinition="MachineToApplication"/>
</configSections>
<appSettings/>
<connectionStrings>
<remove name="LocalSqlServer"/>
<add name="LocalSqlServer"
connectionString="server=(local);
       database=ExtendedMemberShipDb;
       Integrated Security = true" 
providerName="System.Data.SqlClient"/>
</connectionStrings>
<MemberShip defaultProvider="ExtendedSqlMembershipProvider">
<providers>
<add name="ExtendedSqlMembershipProvider" 
type="ExtendedMemberShip.ExtendedSqlMembershipProvider,
          ExtendedMemberShip" 
connectionStringName="LocalSqlServer"
description="Extended MemberShip API"/>
</providers>
</MemberShip>
<system.web>
<compilation debug="true">
<assemblies>
<add assembly="System.Windows.Forms,
             Version=2.0.0.0, Culture=neutral, 
             PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Design, 
             Version=2.0.0.0, Culture=neutral, 
             PublicKeyToken=B03F5F7F11D50A3A"/></assemblies>
</compilation>
<pages>
<controls>
<add namespace="RJS.Web.WebControl" 
assembly="RJS.Web.WebControl.PopCalendar" 
tagPrefix="rjs" />
</controls>
</pages>
<authentication mode="Windows"/>
</system.web>
</configuration>

Listing 9: CreateUserWizard’s CreateUser method

protected void CreateUserWizard1_CreatedUser(object sender, 
EventArgs e)
    {
// In this step, you need to try adding the custom field
// into the custom table
string FirstName = 
Server.HtmlEncode(((TextBox)CreateUserWizard1.CreateUserStep.
ContentTemplateContainer.FindControl("FirstName")).Text);
string LastName = 
           Server.HtmlEncode(((TextBox)CreateUserWizard1.
           CreateUserStep.ContentTemplateContainer.
           FindControl("LastName")).Text);
        DateTime DateOfBirth = 
Convert.ToDateTime(((PopCalendar)CreateUserWizard1.
CreateUserStep.
ContentTemplateContainer.FindControl("PopCalendar1")).
SelectedDate);
// Call the method
bool IsCreated = 
ExtendedMembership.CreateUser(CreateUserWizard1.UserName, 
FirstName, LastName, DateOfBirth);
// Get the label to display results
        Label lblMsg = 
(Label)CreateUserWizard1.CompleteStep.ContentTemplateContainer.
FindControl("lblmsg");
if (!IsCreated)
        {
// User record was not successfully added to my
// custom table
// Delete that user from the membership default tables
// to maintain consistency
            Membership.DeleteUser(CreateUserWizard1.UserName);
// Inform the user
            lblMsg.Text = "User could not be created! 
                           Please try again.<br />";
return;
        }
        lblMsg.Text = "User created successfully! 
                       Thank you for registering with us.<br />";
    }

Listing 10: UpdateUser method in the sample application

protected void updatebtn_Click(object sender, EventArgs e)
    {
// Update the user
string Email = Server.HtmlEncode(this.Email.Text);
        DateTime DateOfBirth = 
Convert.ToDateTime(Server.HtmlEncode(this.PopCalendar1.
SelectedDate));
string Comments = Server.HtmlEncode(this.Comment.Text);
bool isApproved = 
this.IsApproved.SelectedValue.Equals("1")? true : false;
// Execute Update
        MembershipUser _mu = 
Membership.GetUser(this.ddlUserNames.SelectedValue.ToString());
        _mu.Email = Email;
        _mu.Comment = Comments;
        _mu.IsApproved = isApproved;
try
        {
            ExtendMemberShip.Update(new ExtendedMembershipUser(
_mu, null, null, DateOfBirth));
this.ErrorMessage.Text = "User record update
                                      successfully!";
        }
catch
        {
this.ErrorMessage.Text = "User record could not be 
                                      updated!";
        }
this.Panel2.Visible = false;
    }