0%

石榴刺猬 2004-10-04 18:55:00

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    最近开始学习.Net,遇到了一个比较不错的开源的IDE SharpDevelop。这个开发工具是使用C#开发的,比较吸引我的一点就是它是采用了和Eclipse类似的插件技术来实现整个系统的。而这个插件系统是我最感兴趣的地方,因此开始了一段代码的研究。在本篇之后,我会陆续把我研究的心得写下来。由于是在网吧上网,有诸多不便,因此可能会拖比较长的时间。

一、基本概念

    首先,我们先来对 SharpDevelop 有一个比较感性的认识。你可以从这里下载到它的可执行程序和代码包    http://www.icsharpcode.com/  ,安装的废话就不说了,先运行一下看看。感觉跟VS很像吧?不过目前的版本是1.0.0.1550,还有很多地方需要完善。关于代码和系统结构,SharpDevelop的三个作者写了一本书,各位看官可以参考一下,不过我看过之后还是有很多地方不太理解。

    然后,让我来解释一下什么叫插件以及为什么要使用插件系统。我们以往的系统,开发人员编译发布之后,系统就不允许进行更改和扩充了,如果要进行某个功能的扩充,则必须要修改代码重新编译发布。这就给我们带来了比较大的不方便。解决的方法有很多,例如提供配置等等方法。在解决方案之中,插件是一个比较好的解决方法。大家一定知道PhotoShop、WinAmp吧,他们都有“插件”的概念,允许其他开发人员根据系统预定的接口编写扩展功能(例如PhotoShop中各种各样的滤镜)。所谓的插件就是系统的扩展功能模块,这个模块是以一个独立文件的形式出现的,与系统是相对独立。在系统设计期间并不知道插件的具体功能,仅仅是在系统中为插件留下预定的接口,系统启动的时候根据插件的配置寻找插件,根据预定的接口把插件挂接到系统中。

    这样的方式带来什么样的优点呢?首先是系统的扩展性大大的增强了,如果我们在系统发布后需要对系统进行扩充,不必重新编译,只需要修改插件就可以了。其次有利与团队开发,各个功能模块由于是以插件的形式表现在系统中,系统的每日构造就很简单了,不会因为某个模块的错误而导致整个系统的BUILD失败。失败的仅仅是一个插件而已。

    PhotoShop和Winamp的插件系统是比较简单的,他们首先实现了一个基本的系统,然后在这个系统的基础上挂接其他扩展的功能插件。而SharpDevelop的插件系统更加强大,它的整个系统的基础就仅仅是一个插件管理系统,而你看到的所有的界面、功能统统都是以插件的形式挂入的。在这样的一个插件系统下,我们可以不修改基本系统,仅仅使用插件就构造出各种各样不同的系统。

    现在让我们来看看它的插件系统。进入到SharpDevelop的安装目录中,在Bin目录下的SharpDevelop.exe 和 SharpDevelop.Core.dll是这个系统的基本的插件系统。在Addins目录下有两个后缀是addin的文件,其中一个 SharpDevelopCore.addin 就是它的核心插件的定义(配置)文件,里面定义的各个功能模块存在于Bin/Sharpdevelop.Base.dll 文件中,另外还有很多其他的插件定义在Addins目录下的addin文件中。

    分析SharpDevelop的代码,首先要弄清楚几个基本的概念,这些概念和我以前的预想有一些区别,我深入了代码之后才发现我的困惑所在。

1、AddInTree  插件树
    SharpDevelop 中的插件被组织成一棵插件树结构,树的结构是通过 Extension(扩展点)中定义的Path(路径)来定义的,类似一个文件系统的目录结构。系统中的每一个插件都在配置文件中指定了 Extension,通过Extension中指定的 Path 挂到这棵插件树上。在系统中可以通过 AddTreeSingleton对象来访问各个插件,以实现插件之间的互动。

2、 AddIn 插件
    在 SharpDevelop 的概念中,插件是包含多个功能模块的集合(而不是我过去认为的一个功能模块)。在文件的表现形式上是一个addin配置文件,在系统中对应 AddIn 类。

3、Extension 扩展点
    SharpDevelop中的每一个插件都会被挂到 AddInTree(插件树) 中,而具体挂接到这个插件树的哪个位置,则是由插件的 Extension 对象中的 Path 指定的。在addin 配置文件中,对应于 。例如下面这个功能模块的配置

指定了扩展点路径为 /SharpDevelop/Workbench/Ambiences ,也就是在插件树中的位置。

4、Codon
    这个是一个比较不好理解的东西,在 SharpDevelop 的三个作者写的书的中译版中被翻译为密码子,真是个糟糕的翻译,可以跟Handle(句柄)有一拼了。词典中还有一个翻译叫“基码”,我觉得这个也不算好,不过还稍微有那么一点意思。(这里我原来误写为“代码子”,在评论中有位仁兄说这个翻译不错,现在我觉得也好像确实不错 ^o^)
    根据我对代码的理解,Codon 的功能是描述(包装)一个功能模块(一个功能模块对应一个实现了具体功能的 Command 类)。为了方便访问各个插件中的功能模块, Codon 给各种功能定义了基本的属性,分别是 ID (功能模块的标识),Name (功能模块的类型。别误会,这个Name 是addin文件定义中Codon的XML结点的名称,ID才是真正的名称),其中Name可能是Class(类)、MenuItem(菜单项)、Pad(面板)等等。根据具体的功能模块,可以继承Codon定义其他的一些属性,SharpDevelop中就定义了 ClassCodon、MenuItemCodon、PadCodon等等,你可以根据需要自己定义其他类型的Codon。在addin定义文件中,Codon对应于 标签下的内容。例如下面这个定义

<Extension …> 内部定义了一个Codon,<**Class** …>  表示该Codon是一个 Class(类),接着定义了该Codon的 ID和具体实现该Codon的类名ICSharpCode.SharpDevelop.Services.NetAmbience。运行期间将通过反射来找到对应的类并创建出来,这一点也是我们无法在以前的语言中实现的。

再例如这一个定义

这个扩展点中定义了三个菜单项,以及各个菜单项的名字、标签和实现的类名。这里的Codon就对应于系统中的MenuCodon对象。

5、Command 命令
    正如前文所述,Codon描述了一个功能模块,而每个功能模块都是一个 ICommand 的实现。最基本的 Command 是  AbstractCommand,根据Codon的不同对应了不同的 Command。例如 MenuItemCodon 对应 MenuItemCommand 等等。

6、Service 服务
    插件系统中,有一些功能是整个系统都要使用的,例如文件访问、资源、消息等等。这些功能都作为插件系统的一个基本功能为整个系统提供服务,我们就叫“服务”好了。为了便于访问,这些服务都统一通过 ServiceManager 来管理。其实服务也是一种类型的插件,它们的扩展点路径在目录树中的 /Workspace/Services 中。

    理解了这几个基本的概念之后,就可以看看 SharpDevelop 的代码了。从 src/main/startup.cs 看起吧,之后是addin.cs、addinTree.cs 等等。

   写了两个小时了,休息一下。且听下回分解。

在开发Silverlight程序的时候,经常需要在不同的组件间进行通信。比如点击一个button,可能就需要改变另一个控件的内容。比较直接的办法是使用事件,当然使用MVVM的时候也可以使用command,还可以定义一些全局的变量来保存一些信息等。

Prism提供了几种用于组件间通信的途径,可以使用RegionContext使不同的视图共享数据,也可以借助于容器的力量来使用共享的service来进行通信,或者使用command等。除此之外,Prism还提供了一种基于事件的多播发布/订阅方式的通信机制,使不同的组件之间能够以一种松散耦合的方式来进行通信。这就是本文要介绍的事件聚合(Event Aggregation)。

事件聚合的过程有点像收听广播,首先要有个固定的频率,然后内容就会在这个频率上广播出去,至于有没有人收听,广播电台是不知道的,它只是把内容播送了出去。而其他的人想听广播也不用跑到广播电台,只要知道频率,收听这个频率就可以了。联系广播电台和听众的就是这个频率。

在事件聚合的过程中,事件发布方(publisher)相当于广播电台,事件接收方(Subscriber)相当于听众,而事件自然就相当于频率了。

使用Event Aggregation很简单,只需要知道一个接口和一个类基本上就足够了。接口是IEventAggregator,类是CompositePresentationEvent。

要想发布或订阅事件,自然得先要有事件,所以第一件工作就是要定义事件。Prism提供了一个事件基类CompositePresentationEvent,自定义的事件只需要继承这个类就可以了,泛型代表的是事件发生过程中需要传递的参数类型。如:

1
2
3
public class ReceiveNewEmailEvent : CompositePresentationEvent<MailViewModel>
{
}

上面定义了一个事件,用于在收到新邮件时使用,传递的参数是一个邮件的ViewModel。

使用的时候也很简单,使用IEventAggregator接口中的GetEvent方法来获取事件,然后要么发布出去,要么订阅一下就可以了。

下面是当收到一封新的邮件的时候,发布事件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EmailReceiver
{
private IEventAggregator _eventAggregator;
public EmailReceiver(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}

public void ReceiveEmail()
{
if (_email != null)
{ // 当接收到新邮件时,就发布事件,所有订阅了该事件的组件都会接到通知
_eventAggregator.GetEvent<ReceiveNewEmailEvent>()
.Publish(_email);
}
}
}

可以看到我们直接在构造函数中传递了IEventAggregator类型的参数,如果使用Prism来搭建Silverlight程序的话,那么在默认的Bootstrapper中会在容器中添加IEventAggregator的实例,所以并不需要我们做其它更多的工作。如果对Prism或Bootstrapper不太了解的话,可以参考这两篇文章(Prism简介Bootstrapper)。

下面是订阅ReceiveNewEmail事件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MailBox
{
public MailBox(IEventAggregator eventAggregator)
{
eventAggregator.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceivedNewEmail);
}

// 该方法必须为public
public void OnReceivedNewEmail(MailViewModel mail)
{
// do something
}
}

这样,发布出去的事件马上就可以被接收到,而且两个组件只是依赖于事件,彼此之间是松散耦合的。

事件可以订阅,也可以退订,甚至可以有选择地接受某些特定的事件。下面以一个模拟的简单的邮箱客户端来演示一下Event Agregation的使用场景。

image

如图所示,左边是邮件列表,会有一个定时器每隔两秒钟接收到一封邮件,这时邮箱客户端会更新邮件列表,点击左边的列表,会在右边显示邮件的内容。如果点击’将该发信人加入黑名单’,则不会再接受来自该发件人的邮件,如果点击断开连接,则停止接受邮件,再次点击会继续接收邮件。需求大致就是这样了。

首先在启动程序的时候开启一个定时器,每隔两秒钟会接收一封邮件,并发布事件通知有新邮件:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EmailReceiver
{
public void Run()
{
var timer = new DispatcherTimer();
timer.Tick += (s, e) => EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Publish(EmailRepository.GetMail());
timer.Interval = new TimeSpan(0, 0, 0, 2);
timer.Start();
}

}

MailList组件会订阅这个事件,并对邮件列表进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public partial class MailList : UserControl
{
private readonly ObservableCollection<MailViewModel> _mails =
new ObservableCollection<MailViewModel>();

// 黑名单列表
private readonly List<string> _refusedSenders = new List<string>();

public MailList()
{
InitializeComponent();

MailItems.ItemsSource = _mails;

SubscribeReceiveEmailEvent();
}

private void SubscribeReceiveEmailEvent()
{ // 订阅事件的Subscribe方法提供了几个重载方法,除了最简单的直接订阅之外,
// 还可以指定线程类型(比如如果直接使用System.Threading.Timer的话,
// 就必须使用ThreadOption.UIThread,否则会报错),以及是否持有订阅者的引用,
// 或者指定一个filter来对事件进行过滤
// 本例中使用的filter是拒绝接受黑名单中包含的发件人发过来的邮件
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,
true, (mail) => !_refusedSenders.Contains(mail.From));
}

public void OnReceiveNewEmail(MailViewModel mail)
{
_mails.Insert(0, mail);
}
}

当点击左边的邮件列表的时候,会在右边的MailContent组件中显示该邮件的信息,这个过程也是通过Event Aggregation来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  NotificationObject是Prism提供的对MVVM的支持的ViewModel的基类
// 可以简化INotifyPropertyChanged接口的实现方式
public class MailViewModel : NotificationObject
{
public MailViewModel()
{ // DelegateCommand也是Prism提供的一种Command类型
ViewMailCommand = new DelegateCommand(OnViewMail);
}

public ICommand ViewMailCommand { get; private set; }

public void OnViewMail()
{
this.HasRead = true;
EventAggregatorRepository.EventAggregator
.GetEvent<ViewEmailEvent>()
.Publish(this);
}
}

当点击时,会进入相应的Command逻辑,而MailContent则订阅了ViewEmailEvent,并将传递过来的MailViewModel显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial class MailContent : UserControl
{
public MailContent()
{
InitializeComponent();

EventAggregatorRepository.EventAggregator
.GetEvent<ViewEmailEvent>()
.Subscribe(OnViewEmail);
}

public void OnViewEmail(MailViewModel mail)
{
this.DataContext = mail;
}
}

当点击将该发信人加入黑名单按钮时,会发布AddRefuseSenderEvent,而接收到这一事件的MailList组件则会更新黑名单,这样filter就会过滤掉黑名单中已经存在的发件人的邮件:

1
2
3
4
5
6
7
public void OnRefusedSendersAdded(string sender)
{
if (!_refusedSenders.Contains(sender))
{
_refusedSenders.Add(sender);
}
}

如果点击了断开连接或重新连接的话,会发布一个ConnectOrDisconnectMailServerEvent事件。Prism的事件基类并不支持不带参数的事件,也就是说没有办法创建一个不需要传参的事件。所以这里我们使用了object类型作为参数类型,在传递参数的时候直接传了个null过去。

1
2
3
EventAggregatorRepository.EventAggregator
.GetEvent<ConnectOrDisconnectMailServerEvent>()
.Publish(null);

而当MailList接收到该事件的时候,首先判断一下是否已经订阅了ReceiveNewEmailEvent事件,如果订阅了就退订,如果没有订阅就重新订阅。这样来达到开启或关闭接收邮件的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public partial class MailList : UserControl
{
private readonly ObservableCollection<MailViewModel> _mails =
new ObservableCollection<MailViewModel>();

private readonly List<string> _refusedSenders = new List<string>();

public MailList()
{
InitializeComponent();

SubscribeReceiveEmailEvent();

EventAggregatorRepository.EventAggregator
.GetEvent<ConnectOrDisconnectMailServerEvent>()
.Subscribe(OnConnectOrDisconnectMailServer);
}

public void OnConnectOrDisconnectMailServer(object obj)
{
// 判断是否已经订阅了该事件
bool hasSubscribed = EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Contains(OnReceiveNewEmail);
if (hasSubscribed)
{
UnsubscribeReceiveEmailEvent();
}
else
{
SubscribeReceiveEmailEvent();
}
}

private void SubscribeReceiveEmailEvent()
{
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,
true, (mail) => !_refusedSenders.Contains(mail.From));
}

private void UnsubscribeReceiveEmailEvent()
{ // 退订事件
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Unsubscribe(OnReceiveNewEmail);
}

public void OnReceiveNewEmail(MailViewModel mail)
{
_mails.Insert(0, mail);
}
}

由于EventAggregation并不需要建立在Prism装配的程序上,为了操作简便,所以并没有使用Prism来管理这个程序,当然也就没有使用容器。所以我用了一个static的全局变量来保存了一个IEventAggregator的实例。

本文为了演示,所以大量地使用了Event Aggregation,希望大家在工作中要仔细斟酌使用,虽然用起来很灵活,但是如果事件太多的话,也会让人有无从下手的感觉,增加维护的难度。

Prism之Region(1)中,介绍了一些Prism中页面组织以及切换的方式。这一篇将以一个很简单的示例程序来实践一下。

下面是效果图:

image

先说Log,Prism内置了ILogFacade接口,在Prism提供的QuickStart项目里的Modularity中,有一个CallbackLogger,这里我们直接拿过来使用。然后在PrismRegionShell中放一个TextBox,将log的内容显示在这个TextBox中。值得一提的是,为了让输出新log的直接显示出来,需要将TextBox的滚动条滚动到最下面。这里采用的是如下方法:

1
2
3
4
5
6
public void Log(string message, Category category, Priority priority)
{
this.LogContainer.Text += string.Format(CultureInfo.CurrentUICulture, "[{0}][{1}] {2}\r\n", category, priority, message);
// 这段代码的作用是让文本框的滚动条滚动到最底部
LogContainer.Select(LogContainer.Text.Length, LogContainer.Text.Length);
}

然后说一下左边的导航区。这里放置了一个ItemsControl,并将其设为Region。里面的两个按钮并不是直接写死到xaml里的,而是在两个Module初始化时动态添加进来的。也就是说,这个ItemsControl并不知道自己将要包含哪些项。这里我们使用IRegionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(EmptyNavigationItem));这种方式来将视图注册到Region中。这样当该Region显示的时候两个视图才会被初始化。这里需要注意的是,一个Region里需要同时显示多个视图时,视图的顺序问题。比如ItemsControl,哪个先被注册就哪个显示在上面,但是由于Module的加载速度等原因,所以这时两个视图不一定谁在上面。现在我需要指定[导航示例]这个按钮在上,那么Prism为我们提供了ViewSortHintAttribute来解决这个问题。在需要进行排序的视图上添加上相应的attribute就可以了。

1
2
3
4
5
[ViewSortHint("01")]
public partial class NavigationItem : UserControl

[ViewSortHint("02")]
public partial class EmptyNavigationItem : UserControl

在初始化导航实例的Module时,将导航示例的视图注册到内容区的Region,这时[上一个]按钮依然处于灰色状态,因为通过RegisterViewWithRegion方法显示的页面是不被记录的。当点击[ViewA][ViewB][ViewC]这三个按钮时,会采用RequestNavigate方法来进行页面的跳转,这时页面跳转的过程会被记录下来,此时就可以通过[上一个]和[下一个]按钮进行页面的前进和后退。

1
2
3
4
5
6
7
8
9
10
11
12
void ToSpecifiedView(string viewName)
{
UriQuery query = new UriQuery();
if (viewName == ViewNames.ViewA)
{
query.Add("Time", DateTime.Now.ToShortTimeString());
}
Uri uri = new Uri(viewName + query.ToString(), UriKind.Relative);
_regionManager.RequestNavigate(RegionNames.NavDemoShowRegion, uri);
logger.Log("跳转到视图 [" + viewName + "]", Category.Info, Priority.Low);
ResetNavigationButtonState();
}

注意这三个视图已经在初始化Module的时候使用IUnityContainer.RegisterType<object, ViewA>(ViewNames.ViewA)方法注册过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Initialize()
{
logger.Log("初始化Navigation模块", Category.Debug, Priority.Low);
_regionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(NavigationItem));
_regionManager.RegisterViewWithRegion(RegionNames.MainRegion,
() => _container.Resolve<NavigationContainer>() );
_regionManager.RegisterViewWithRegion(RegionNames.NavDemoActionRegion, typeof(ActionController));

// 注意注册的类型的必须是object,因为Prism无法确定视图的类型,所以就用了object
_container.RegisterType<object, ViewA>(ViewNames.ViewA);
_container.RegisterType<object, ViewB>(ViewNames.ViewB);
_container.RegisterType<object, ViewC>(ViewNames.ViewC);
}

ViewA和ViewB都实现了INavigationAware接口,不同之处在于ViewA是在其对应的ViewModel ViewAViewModel类中实现的,而ViewB则直接在Code Behind中实现的。Prism对MVVM提供了良好的支持,因此既可以选择在视图中实现该接口也可以在对应的ViewModel中实现。

1
2
3
4
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}

在ViewB中,IsNavigationTarget方法返回了false,而ViewA中则返回了true。可以通过点击三个按钮进行页面跳转,观察log可以发现,ViewA只创建了一次,而ViewB则每次都要重新创建。还有就是在跳转到ViewA的时候传递了参数,可以在OnNavigatedTo方法中取出参数。

1
2
3
4
5
6
public void OnNavigatedTo(NavigationContext navigationContext)
{
UriQuery query = navigationContext.Parameters;
string time = query["Time"];
logger.Log(string.Format("ViewA: 现在时间 {0}", time), Category.Info, Priority.Medium);
}

Prism可以帮助我们开发模块化程序,将程序分割成一个个独立的Module,分别进行开发。然后在程序运行的时候,将各个Module组合到一起,为程序提供各种各样的功能。通常来说,Module是一些视图和功能的集合,那么就需要一种办法来将这些视图以某种形式,在特定的时间展现出来。Prism通过Shell + Region来组织视图的布局,完成视图间的转换等。

image

如上图所示,Shell相当于ASP.NET中的母版页,它定义了页面的布局、主题等。其中的导航区和内容区是预留出来的需要进行填充内容的部分,也就是Region,起到占位符的作用,程序会在运行时动态地向Region中填充内容。

那么如何将一个区域定义为Region呢?

首先在引入Prism的命名空间

xmlns:prism="http://www.codeplex.com/prism" 如果IDE无法找到这个命名空间的话,需要先注册Prism

然后在需要定义为Region的控件上加上Attached Property。

<ContentControl prism:RegionManager.RegionName="MainRegion" />

并不是所有的控件都可以作为Region的,需要为需要定义为Region的控件添加RegionAdapter。RegionAdapter的作用是为特定的控件创建相应的Region,并将控件与Region进行绑定,然后为Region添加一些行为。一个RegionAdapter需要实现IRegionAdapter接口,如果你需要自定义一个RegionAdapter,可以通过继承RegionAdapterBase类来省去一些工作。Prism为Silverlight提供了几个RegionAdapter:

  • ContentControlRegionAdapter: 创建一个SingleActiveRegion并将其与ContentControl绑定
  • ItemsControlRegionAdapter: 创建一个AllActiveRegion并将其与ItemsControl绑定
  • SelectorRegionAdapter: 创建一个Region并将其与Selector绑定
  • TabControlRegionAdapter: 创建一个Region并将其与TabControl绑定

从图中可以看到,导航区对应的NavigationRegion中四个视图都是亮着的,而内容区对应的ContentRegion中四个视图只有一个是亮着的(橘黄色代表显示在页面中)。ItemsControl本来就是由许多个Item组成的,因此ItemsControlRegionAdapter会创建AllActiveRegion,这种类型的Region中所有Active的视图都会显示在ItemsControl中;而ContentControl只能容纳一个Content,所以ContentControlRegionAdapter创建了一个SingleActiveRegion,其中的视图只有一个是处于Active状态的,会显示在ContentControl中,其它的都是不可见的,需要将它们激活(Active),才能使其显示。

通常我们并不直接和Region打交道,而是通过RegionManager,它实现了IRegionManager接口。IRegionManager接口包含一个只读属性Regions,是Region的集合,还有一个CreateRegionManager方法。Prism通过RegionManagerExtensions类使用扩展方法为IRegionManager添加了更多的功能。

  • AddToRegion: 将一个视图添加到一个Region中。
  • RegisterViewWithRegion: 将一个视图和一个Region进行关联。当Region显示的时候,关联的视图才会显示,也就是说,在这个Region显示之前,关联的视图是不会被创建的。
  • RequestNavigate: 进行页面切换,将指定的Region中显示的视图切换为指定的视图。

本文开头说过,需要在运行时将分散在各个Module的视图显示在页面特定的位置上。那么首先就需要定义页面显示的地方,即Region。然后就是要定义创建视图的时机和方式。在Prism中有两种方式来定义视图与Region之间的映射关系——View Discovery和View Injection。

View Discovery是以声明式的方式来建立Region和视图之间的关系。如上图中的导航区,需要在导航区显示的时候就将各个导航视图填充到其中。而内容区中也需要一个默认显示的内容视图。因此也可以这样理解View Discovery,就是指定一个Region的默认视图。我们可以使用IRegionManager.RegisterViewWithRegion方法来声明某个Region默认应该显示哪个视图。注意这里是Register,是注册,也就是说不会马上创建该视图。当Region显示在页面中的时候,它会去寻找与自己相关联的视图,并对其进行初始化。

1-30-2011 5-03-27 PM

1-30-2011 5-04-22 PM

这样做的好处是我们不必关注在什么时候创建视图,一切都会自动完成。缺点就是默认视图是确定的,当需要进行视图转换的时候,这种方式就行不通了。这时候就需要View Injection。

View Injection可以让我们对于Region中显示的视图有更精确的控制。通常可以通过调用IRegionManager.AddToRegion方法或者是IRegionManager.Regions[“RegionName”].Add方法来向一个Region中添加一个视图的实例。对于SingleActiveRegion(ContentControlRegionAdapter会创建这种类型的Region),可以通过IRegion.Activate方法将一个已经添加到Region中的视图显示出来。当然也可以通过IRegion.Deactivate方法来将视图状态置为非激活或者干脆调用IRegion.Remove方法将视图移除。可以看到,因为要添加的是视图的实例,所以需要仔细地设计在什么时候使用View Injection,以免造成不必要的开销。

在Prism 4.0中新添加了一些导航API,这套API大大地简化了View Injection的流程,它使用URI来进行Region中视图的导航,然后会根据URI来创建视图,并将其添加到Region中,然后激活该视图。导航API的出现不只是为了简化View Injection的过程,它还提供了前进、后退的功能,并且对MVVM模式下的导航有良好的支持,还能够在进行导航的时候传递参数等等。所以推荐的方式是使用新的导航API,也就是使用IRegionManager.RequestNavigate方法。

如果一个页面相对来说不大变化,如导航区,在程序初始化的过程完成后就不会轻易地变动,这时候就较适合于使用RegisterViewWithRegion方法,通常可以在Module的Initialize方法中完成这个过程。

1
2
3
4
5
6
7
8
public void Initialize()
{
logger.Log("初始化Navigation模块", Category.Debug, Priority.Low);
_regionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(NavigationItem));
_regionManager.RegisterViewWithRegion(RegionNames.MainRegion, // 两种方式都可以
() => _container.Resolve<NavigationContainer>() );
_regionManager.RegisterViewWithRegion(RegionNames.NavDemoActionRegion, typeof(ActionController));
}

如果一个区域需要频繁地切换页面的话,如主内容区,可以使用View Injection的方式。

1
2
3
4
IRegionManager regionManager = ...;
IRegion mainRegion = regionManager.Regions["MainRegion"];
InboxView view = this.container.Resolve<InboxView>();
mainRegion.Add(view);

可以看到,这时候已经生成了视图的实例。之前提到过,一个Region可以包含多个视图,这些视图会处于不同的状态,对于ItemsControl类型的Region来说,里面会显示很多个Item,所以添加进去就可以了;但是对于ContentControl这种Region,同一时刻只能显示一个视图,所以在添加进去之后还需要有一个Activate的过程。

使用URI来进行导航只需要提供需要切换的视图的名称就可以,并不需要了解视图的类型,从而达到解耦的目的,并且可以通过URI来进行参数传递。

1
2
3
4
5
6
7
8
public void Initialize()
{
// 因为Prism无法确定每个视图都是什么类型,所以就使用了Object,
// 因此在根据ViewName获取实例时,会使用IServiceLocator.GetInstance<Object>(ViewName)
_container.RegisterType<object, ViewA>(ViewNames.ViewA);
_container.RegisterType<object, ViewB>(ViewNames.ViewB);
_container.RegisterType<object, ViewC>(ViewNames.ViewC);
}

首先注册一下视图的类型,其实就是将视图的名称与视图类型进行一下关联。在导航的时候调用RequestNavigate方法就可以了。

1
2
3
4
5
6
void ToSpecifiedView(string viewName)
{
Uri uri = new Uri(viewName, UriKind.Relative);
_regionManager.RequestNavigate(RegionNames.NavDemoShowRegion, uri);
logger.Log("跳转到视图 [" + viewName + "]", Category.Info, Priority.Low);
}

Prism提供了UriQuery类来帮助我们在导航的时候传递参数。

1
2
3
4
5
6
7
8
9
10
void ToSpecifiedView(string viewName)
{
UriQuery query = new UriQuery();
if (viewName == ViewNames.ViewA)
{
query.Add("Time", DateTime.Now.ToShortTimeString());
}
Uri uri = new Uri(viewName + query.ToString(), UriKind.Relative);
_regionManager.RequestNavigate(RegionNames.NavDemoShowRegion, uri, CallbackHandler); // 回调方法可加可不加
}

上面的代码判断当跳转到ViewA时,传递一个叫做Time的参数。那么怎样在视图中获取传递的参数呢?这里就要提一下INavigationAware接口了。这个接口使视图或者其对应的ViewModel也可以参与到页面导航的过程中来。所以这个接口既可以由视图来实现,也可以由视图的DataContext——通常指的就是ViewModel,来实现。

1
2
3
4
5
6
public interface INavigationAware
{
bool IsNavigationTarget(NavigationContext navigationContext);
void OnNavigatedTo(NavigationContext navigationContext);
void OnNavigatedFrom(NavigationContext navigationContext);
}

当从本页面转到其它页面的时候,会调用OnNavigatedFrom方法,navigationContext会包含目标页面的URI。

当从其它页面导航至本页面的时候,首先会调用IsNavigationTarget,IsNavigationTarget返回一个bool值,简单地说这个方法的作用就是告诉Prism,是重复使用这个视图的实例还是再创建一个。然后调用OnNavigatedTo方法。在导航到本页面的时候,就可以从navigationContext中取出传递过来的参数。

image

使用导航API的另一个优点就是可以进行页面的前进和后退,一切由Prism完成。这个功能是由IRegionNavigationJournal接口提供的。

1
2
3
4
5
6
7
8
9
10
11
public interface IRegionNavigationJournal
{
bool CanGoBack { get; }
bool CanGoForward { get; }
IRegionNavigationJournalEntry CurrentEntry { get; }
INavigateAsync NavigationTarget { get; set; }
void Clear();
void GoBack();
void GoForward();
void RecordNavigation(IRegionNavigationJournalEntry entry);
}

其中CanGoBack和CanGoForward属性表示当前是否可以后退或前进。如果可以的话,可以使用GoBack和GoForward方法进行前进和后退。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ActionControllerViewModel : NotificationObject
{
private IRegion _demoShowRegion;
public bool CanGoBack
{
get
{
return _demoShowRegion.NavigationService.Journal.CanGoBack;
}
}

public bool CanGoForward
{
get
{
return _demoShowRegion.NavigationService.Journal.CanGoForward;
}
}

void ToPrevious()
{
_demoShowRegion.NavigationService.Journal.GoBack();
ResetNavigationButtonState();
}

void ToNext()
{
_demoShowRegion.NavigationService.Journal.GoForward();
ResetNavigationButtonState();
}

void ResetNavigationButtonState()
{
RaisePropertyChanged(() => this.CanGoBack);
RaisePropertyChanged(() => this.CanGoForward);
}
}

image

导航API还可以控制视图的生命周期,在页面跳转时进行确认拦截(Confirming or Cancelling Navigation)以及其它功能,可以参考 Developer’s Guide to Microsoft Prism

Prism的核心功能之一就是支持模块化应用程序开发(Modular Application Development),并且在运行时对各个模块进行动态管理。

使用Prism进行模块化开发首先要了解几个概念:

1.Module: Module是一些逻辑上相关的程序集或者资源文件的集合,在Silverlight程序中通常以xap文件为单位存在。而每一个Module中都需要有一个负责进行初始化工作以及与系统进行集成的角色,它需要实现IModule接口。IModule接口中只有一个Initialize方法,一方面这个接口将这个工程标记为一个Module,另一方面你可以在Initialize方法中实现一些逻辑,比如向容器中注册一些Service,或者将视图集成到程序中等等。

2.ModuleInfo: 在创建了一个Module之后,需要通知Prism这个Module的存在,也就是要注册一下。在Prism中,Module是以ModuleInfo的形式存在的。ModuleInfo记录了Module的信息,ModuleName属性是Module的标识符,相当于Module的ID;ModuleType是Module的AssemblyQualifiedName;DependsOn属性是该Module依赖的其它Module的ModuleName的集合,在加载该Module时,如果有依赖项没有加载的话,会先将依赖项加载;InitializationMode,有两种情况——WhenAvailable和OnDemand,当选择了WhenAvailable时,该Module会在程序启动时自动加载,如果选择了OnDemand,则会按需加载,默认情况下是WhenAvailable;Ref,存储该Module的位置,如XXX.xap;State,定义了Module从注册到加载到初始化的整个过程中的状态。

3.ModuleCatalog: ModuleCatalog实现了IModuleCatalog接口,它是ModuleInfo的容器,保存着系统中所有Module的信息,不仅会管理哪些Module需要加载,什么时候加载以什么顺序加载等问题,还要检查各个Module之间是否存在着循环依赖、是否有重复的Module等等。ModuleCatalog提供了含参构造方法和AddModule方法,可以通过代码将Module注册进去,同时也可以在xaml文件中配置好Module,然后通过ModuleCatalog.CreateFromXaml方法来加载。

4.ModuleManager: ModuleManager实现了IModuleManager接口。顾名思义就是管理Module的类。IModuleManager中含有两个方法和两个事件:Run方法会将所有InitializationMode为WhenAvailable的Module加载,然后进行初始化,初始化的工作委托给了IModuleInitializer来完成,它会获取到Module类(上面提到的实现了IModule接口的类)的实例,然后调用其Initialize方法。LoadModule方法用来加载InitializationMode为OnDemand的Module。两个事件分别用来通知下载Module的进度变化以及Module加载完成。

下面用一个示例程序来说明如何在Prism中进行模块化程序开发。

1.创建一个Silverlight Application,叫做PrismModule。

2.在Solution中添加三个Silverlight Application,分别叫做ModuleA, ModuleB, ModuleC。然后删除这三个工程中的App文件和MainPage文件。

3.在ModuleA工程下添加一个UserControl,叫做ViewA,然后再添加一个类,叫做ModuleA。并添加Microsoft.Practices.Prism和Microsoft.Practices.ServiceLocation引用。下面是ViewA和ModuleA的代码:

1
2
3
4
5
6
7
8
9
10
11
12
<UserControl x:Class="ModuleA.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="Module A" FontSize="22" />
</Grid>
</UserControl>
1
2
3
4
5
6
public class ModuleA : IModule
{
public void Initialize()
{
}
}

4.对ModuleB和ModuleC重复做步骤3的操作,只是将文本改成相应模块。

5.在PrismModule中添加对ModuleA、ModuleB、ModuleC、Prism、UnityExtensions还有Unity for Silverlight的引用,然后创建Shell和Bootstrapper。添加一个UserControl,叫做Shell;再添加一个类,叫做Bootstrapper。

Shell代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<UserControl x:Class="PrismModule.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">

<StackPanel Margin="50">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionA" />
</Border>
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionB" />
</Border>
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionC" />
</Border>
<Button Content="Load Module C" Click="LoadModuleC" Width="120" Height="25" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

Shell.xaml.cs代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<UserControl x:Class="PrismModule.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">

<StackPanel Margin="50">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionA" />
</Border>
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionB" />
</Border>
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionC" />
</Border>
<Button Content="Load Module C" Click="LoadModuleC" Width="120" Height="25" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

Bootstrapper代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.TryResolve<Shell>();
}

protected override void InitializeShell()
{
App.Current.RootVisual = (UIElement)this.Shell;
}

protected override IModuleCatalog CreateModuleCatalog()
{
return new ModuleCatalog();
}

protected override void ConfigureModuleCatalog()
{
Type typeA = typeof(ModuleA.ModuleA);
ModuleInfo moduleA = new ModuleInfo
{ // ModuleA没有设置InitializationMode,默认为WhenAvailable
ModuleName = typeA.Name,
ModuleType = typeA.AssemblyQualifiedName,
};

Type typeB = typeof(ModuleB.ModuleB);
ModuleInfo moduleB = new ModuleInfo
{
ModuleName = typeB.Name,
ModuleType = typeB.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand,
};

Type typeC = typeof(ModuleC.ModuleC);
ModuleInfo moduleC = new ModuleInfo
{
ModuleName = typeC.Name,
ModuleType = typeC.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand,
// ModuleC依赖于ModuleB
DependsOn = new Collection<string> { moduleB.ModuleName },
};

this.ModuleCatalog.AddModule(moduleA);
this.ModuleCatalog.AddModule(moduleB);
this.ModuleCatalog.AddModule(moduleC);
}
}

将App.xaml.cs中的Application_Startup方法改为

1
2
3
4
5
private void Application_Startup(object sender, StartupEventArgs e)
{
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run();
}

6.现在已经有了Region,需要将各个Module中的View填充到Region中。修改ModuleA,ModuleB和ModuleC的Initialize方法。

1
2
3
4
5
public void Initialize()
{
ServiceLocator.Current.GetInstance<IRegionManager>().
RegisterViewWithRegion("RegionA", typeof(ViewA));
}

将其中的A改为相应的字母。运行程序,结果如下:

image

我们点击按钮来加载ModuleC,因为ModuleC依赖于ModuleB,所以ModuleB也一块儿加载出来了。但是这与我们预期的效果不太一致。因为一共只load了一个xap文件,用WinRAR打开看一下,发现三个Module的程序集都在其中。

image

在Silverlight程序中,模块化程序开发应该不仅仅体现在开发时的模块化,运行时也应该是模块化的。比如ModuleA在程序加载时就load出来,但是ModuleB和ModuleC则是在点击了按钮后才load出来的,换句话说,在没点按钮前就不应该将ModuleB和ModuleC的程序集加载进来。现在由于PrismModule项目引用了三个Module,所以程序集会被一块打包进xap文件中。我们修改一下,将对ModuleB和ModuleC的引用的Copy Local属性设置为false:

imageimage

重新编译一下,再次查看xap文件,发现已经没有了ModuleB和ModuleC。

image

运行程序,报错。很简单,因为我们在Bootstrapper中用到了ModuleB和ModuleC,缺少了这两个dll,程序没法运行。为了解决这个问题,我们把初始化ModuleCatalog的过程改一下,不使用代码,而是使用配置文件。在Silverlight中,Prism支持使用xaml文件作为配置文件。下面在PrismModule工程下新建一个资源文件,ModuleCatalog.xaml。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">

<Modularity:ModuleInfo Ref="ModuleA.xap" ModuleName="ModuleA"
ModuleType="ModuleA.ModuleA, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

<Modularity:ModuleInfo Ref="ModuleB.xap" ModuleName="ModuleB" InitializationMode="OnDemand"
ModuleType="ModuleB.ModuleB, ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

<Modularity:ModuleInfo Ref="ModuleC.xap" ModuleName="ModuleC" InitializationMode="OnDemand"
ModuleType="ModuleC.ModuleC, ModuleC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleB</sys:String>
</Modularity:ModuleInfo.DependsOn>
</Modularity:ModuleInfo>
</Modularity:ModuleCatalog>

这里大体和用代码写一致,只不过Ref属性里要写明该Module对应的是哪个xap包。Prism在Silverlight程序中使用一个叫做XapModuleTypeLoader的类来加载Module,在将Module下载之后会获取AppManifest.xaml文件,也就是说如果你的Module是个类库工程的话,会在加载时产生错误。可以将几个类库的程序集文件包装在一个xap文件中作为一个Module来使用,或者自定义一个ModuleTypeLoader。

定义完Module的配置文件后,要改写Bootstrapper。首先删除用代码配置Module的方法ConfigureModuleCatalog,然后在CreateModuleCatalog方法中替换成一下内容:

1
2
3
4
5
protected override IModuleCatalog CreateModuleCatalog()
{
return Microsoft.Practices.Prism.Modularity.ModuleCatalog.CreateFromXaml(
new Uri("/PrismModule;component/ModuleCatalog.xaml", UriKind.Relative));
}

再次运行程序,正常运行。

image

这样就达到了按需加载的目的。节约带宽是一个好处,如果产品是分模块往外卖的时候,可以由客户按需定制。

不过再打开ModuleB和ModuleC的xap文件看一下,发现里面不仅有Module本身的程序集,还包括了引用的Prism的程序集等。而这些程序集其实已经在PrismModule.xap中包含了。完全没有必要重复下载。所以可以将多余的程序集的引用的Copy Local属性设置为false,这样就瘦身成功了。(想要避免重复加载相同的文件,也可以通过在项目的Properties面板中勾选Reduce XAP size by using application library caching选项)

如果你对Module的加载到执行的整个过程感兴趣,那么Prism本身提供了一个QuickStart,既有Unity版本也有Mef版本,不要错过。

在程序中使用框架必然要有一个切入点,框架会在这里进行初始化,处理相关配置信息等。在Prism中扮演这一角色的就是Bootstrapper。

Prism提供了一个抽象基类Bootstrapper,这个类里面包含了包含了许多空的虚方法,可以重写它们添加自己的逻辑。这个基类与任何容器无关,所以可以通过继承它来实现基于特定容器的Bootstrapper,不过通常我们大可不必这样做,因为Prism默认提供了两个基于特定容器的Bootstrapper——UnityBootstrapper和MefBootstrapper,分别使用Unity和Mef来实现依赖注入。而我们需要做的工作就是在这两个类之间选择一个适合自己的,稍微配置一下就可以了。当然如果你不喜欢这两个容器或者已有的程序使用了其它容器(如Spring.Net, Castle等),也可以通过继承Boostrapper抽象基类来实现自己的SpringBootstrapper和CastleBootstrapper。虽然UnityBootstrapper的代码看起来挺简单的,但是如果仿照这个来实现CastleBootstrapper却并不是那么容易的一件事(不信你可以试试),所以更好的办法是用现成的。

那么Bootstrapper都做了些什么呢?
  1. 创建Logger:

    执行CreateLogger方法,默认创建一个EmptyLogger,不会在任何地方输出log。当然是可以扩展的,比如你可以使用Clog来做一个适配器。

  2. 创建并配置ModuleCatalog

执行CreateModuleCatalog方法,默认创建一个空的ModuleCatalog。然后执行ConfigureModuleCatalog方法,默认情况下这个方法是空的。可以重写这两个方法,加入自定义的获取ModuleCatalog的逻辑,比如在CreateModuleCatalog中可以从一个xaml文件中读取Module信息。

1
2
3
4
5
protected override IModuleCatalog CreateModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Uri("/AssemblyName;component/ModulesCatalog.xaml", UriKind.Relative));
}

  1. 创建并配置依赖注入容器

Prism中使用依赖注入来管理各个组件,你可以使用任何你熟悉的容器,比如Castle, Unity等。Prism中内置了对Unity以及Mef的支持,所以有两种预定义好的Bootstrapper: UnityBootstrapper和MefBootstrapper,其中分别采用UnityContainer和CompositionContainer作为依赖注入容器。以UnityBootstrapper为例,在这一步中会先调用CreateContainer方法,返回一个UnityContainer;然后调用ConfigureContainer方法,在这个方法中主要是将一些常用的类注册到容器中。

  1. 配置默认的Region适配器映射

为了使xaml中的UI控件可以使用Region,需要先注册一下。Prism默认支持Region的控件类型有:TabControl, Selector, ItemsControl, ContentControl。当然你也可以通过实现IRegionAdapter接口或者直接继承RegionAdapterBase来使其它控件也支持Region。

  1. 配置默认的Region 行为(Behavior)

为RegionBehaviorFactory添加一些默认的行为。这样可以扩展Region的行为。可以通过实现IRegionBehavior接口或继承RegionBehavior来自定义Region的行为,并重写ConfigureDefaultRegionBehaviors方法添加到Region。

  1. 注册框架异常类型

Prism提供了ExceptionExtensions类来帮助开发人员定位异常发生的根异常。在这一步通过调用RegisterFrameworkExceptionTypes方法向ExceptionExtensions中添加新的Root Exception。

  1. 创建并初始化Shell

首先调用CreateShell方法来创建一个Shell,这是一个抽象方法,通常这个方法中就是返回作为整个网站容器的页面。之后会将RegionManager attach到Shell上,然后更新定义的Regions,最后调用InitializeShell方法来初始化Shell。默认情况下这是个空方法,可以通过重写这个方法加入自定义的逻辑,可以在这个方法中将Shell作为Silverlight程序的根容器页面显示出来。

1
2
3
4
5
protected override void InitializeShell()`
{
Application.Current.RootVisual = Shell;
}

  1. 初始化Modules

调用InitializeModules方法,实际上就是调用ModuleManager.Run方法,会调用ModuleCatalog中的所有InitializationMode为WhenAvailable的Module的Initialize方法。

至此,整个容器的初始化过程就完毕了。

值得一提的还有CommonServiceLocator,这同样是Patterns & Practices小组的产品。它的作用很简单,就是统一依赖注入容器的接口,使程序不必依赖于特定的容器,只需要使用ServiceLocator,然后去间接地使用其它各种各样的容器。在Prism的内部就是使用ServiceLocator来进行管理的。所以不管使用什么样的容器,都需要提供一个实现了IServiceLocator接口的适配器,如使用Unity要提供UnityServiceLocatorAdapter,使用Mef要提供MefServiceLocatorAdapter。这样不管外部使用什么容器,内部都不需要改变。所以如果要使用Prism重头开始构架一个程序,那么在整个程序中不依赖于特定的依赖注入容器接口,而是使用ServiceLocator是一个不错的选择,这样可以在需要的情况下很容易地更换容器,只需要重写一个Bootstrapper和一个ServiceLocatorAdapter就可以了。

Prism是由微软Patterns & Practices团队开发的项目,目的在于帮助开发人员构建松散耦合的、更灵活、更易于维护并且更易于测试的WPF应用或是Silverlight应用以及Windows Phone 7应用。使用Prism可以使程序开发更趋于模块化,整个项目将由多个离散的、松耦合的模块组成,而各个模块又可以又不同的开发者或团队进行开发、测试和部署。目前Prism的最新版本是Prism 4,于2010年11月12日发布。Prism有很完整的文档以及丰富的示例程序。在这里我们仅针对于Silverlight程序的开发。

在下载Prism安装包并安装完成后,会在目标文件夹中发现很多文件。

推荐首先运行RegisterPrismBinaries.bat文件,这样在开发基于Prism的程序时可以更方便地添加引用程序集。

使用Prism之前,需要了解一些概念,下面通过一个非常简单的小程序来了解一下Prism。

1.打开Visual Studio 2010,新建一个Silverlight Application项目,并添加对Prism的引用。再创建三个Silverlight类库工程。

2.在Contract工程下新建一个接口,叫做ITextProvider。

1
2
3
4
5

public interface ITextProvider
{
string GetText();
}

3.在其它的三个项目中都引用Contract项目。

4.在PrismStarter工程下新建一个TextProvider类并实现ITextProvider接口。

1
2
3
4
5
6
7
8
9
10
11
public class TextProvider : ITextProvider
{
private int i = 0;

public string GetText()
{
i++;
return string.Format("From TextProvider [{0}]", i);
}
}

5.删除PrismStarter项目中自动生成的MainPage.xaml,创建一个新的UserControl,叫做Shell。页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<UserControl x:Class="PrismStarter.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="100" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>

<TextBlock FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center" Text="Prism Starter" />

<ContentControl Grid.Row="1" HorizontalContentAlignment="Stretch" prism:RegionManager.RegionName="RegionA" />

<ContentControl Grid.Row="2" HorizontalContentAlignment="Stretch" prism:RegionManager.RegionName="RegionB" />
</Grid>
</UserControl>

6.在ModuleA工程中添加对Prism程序集的引用。并添加一个UserControl叫做ViewA,页面代码为:

1
2
3
<Grid :Name="LayoutRoot" Background="White">
<TextBlock x:Name="textModuleA" FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>

CodeBehind中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class ViewA : UserControl
{
public ViewA(ITextProvider textProvider)
{
InitializeComponent();

this.Loaded += (s, e) =>
{
textModuleA.Text = string.Format("Module A {0}", textProvider.GetText());
};
}
}

7.在ModuleA工程中添加一个类叫做ModuleA,并实现接口IModule。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ModuleA : IModule
{
private IRegionManager _regionManager;

public ModuleA(IRegionManager regionManager)
{
_regionManager = regionManager;
}

public void Initialize()
{
_regionManager.RegisterViewWithRegion("RegionA", typeof(ViewA));
}
}

注意这里的RegionA对应于Shell页面中的RegionName。

8.在ModuleB工程中重复6、7过程,只是将A替换为B。

9.在PrismStarter工程中添加对ModuleA和ModuleB的引用。

10.在PrismStarter工程中添加一个PrismStarterBootstrapper类,并继承UnityBootstrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class PrismStarterBootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.TryResolve<Shell>();
}

protected override void InitializeShell()
{ // 控制页面在初始化时显示Shell页面
App.Current.RootVisual = (UIElement)this.Shell;
}

protected override void ConfigureModuleCatalog()
{ // 注册Module。在实际开发中可以使用xaml做配置文件,
// 这样就可以将PrismStarter与ModuleA和ModuleB完全解耦,也就不再需要引用这两个项目
Type moduleAType = typeof(ModuleA.ModuleA);
ModuleInfo moduleA = new ModuleInfo
{
ModuleName = moduleAType.Name,
ModuleType = moduleAType.AssemblyQualifiedName,
};

Type moduleBType = typeof(ModuleB.ModuleB);
ModuleInfo moduleB = new ModuleInfo
{
ModuleName = moduleBType.Name,
ModuleType = moduleBType.AssemblyQualifiedName,
};

this.ModuleCatalog.AddModule(moduleA);
this.ModuleCatalog.AddModule(moduleB);
}

protected override void ConfigureContainer()
{ // 注册一下TextProvider,这样在通过容器请求ITextProvider时会返回TextProvider实例
base.ConfigureContainer();
this.Container.RegisterInstance<ITextProvider>(new TextProvider());
}
}

11.最后一步,打开App.xaml.cs,修改Application_Startup方法

1
2
3
4
5
private void Application_Startup(object sender, StartupEventArgs e)
{
PrismStarterBootstrapper bootstrapper = new PrismStarterBootstrapper();
bootstrapper.Run();
}

运行程序,结果如下:

image

下面简单介绍一下这个小例子中涉及到的一些概念。

Bootstrapper: 在程序中使用框架需要找到一个切入点,将框架植入进去,将一部分功能委托给框架来实现。在Silverlight中使用Prism的切入点就是App.xaml.cs中的Application_Startup方法。一般来说,这个方法中只是指定页面最先加载的页面,但是我们把默认的逻辑去掉,取而代之的是Bootstrapper(在本例中就是PrismStarterBootstrapper)。当调用Bootstrapper.Run方法时,它会完成一些准备工作,如一些配置等。因此你会发现,使用Prism后,启动程序时会比正常启动要慢一些,就是因为Bootstrapper做了许多工作。

Container: 依赖注入容器。在程序中使用依赖注入的好处到处都可以找的到。在Silverlight中使用容器来管理各个组件的一个很明显的好处就是使用单例来降低内存使用。否则每次加载一个页面都需要重新创建一个也很耗费资源的。当然好处不只这些,通过容器来注入一些服务(如本例中的IRegionManager和ITextProvider)显得相当方便。

Module: Prism帮助我们把程序分解成一个个功能模块,这些功能模块就叫做Module,通常一个工程就是一个Module。由于Module彼此是独立的,但是在运行时需要将它们整合到一起,因此Prism需要知道Module的存在,这里就涉及到了ModuleCatalog, ModuleCatalog就是Module的容器,里面包含了所有Module的信息,以ModuleInfo的形式存在。ModuleInfo就是对Module的抽象,包含Module的名字,类型,依赖等一些信息。

Shell: 相当于程序的入口,初始界面,还能够提供类似ASP.Net中的母版页的功能。Shell必须由Bootstrapper创建,因为Shell需要使用的一些service,比如RegionManager等,需要在Shell显示前注册。

Region: 相当于ASP.Net中的ContentPlaceHolder(是这么叫的吧?),起到占位符的作用,如本例中Shell中有两个Region——RegionA和RegionB,定义了两块区域。在Module的初始化过程中,通过IRegionManager将Module中的页面放进了定义好的Region中。IRegionManager负责管理Region,可以通过它向Region中注册View,进行导航等。

Prism的功能当然远不止这么简单,它还提供对MVVM模式的支持,对导航的支持等,在后续文章中会逐步介绍。希望能够通过本文让大家对Prism有一定的了解。

代码下载

DotNet

C#开源系统大汇总 - 心存善念 - 博客园

Npoi操作excel - 张龙豪 - 博客园

.NET Core 跨平台发布(dotnet publish) - LineZero - 博客园

如何用.net c# 读取epub格式文件 - szliszt的专栏 - 博客频道 - CSDN.NET

使用epublib自动生成epub文件 - zhyoulun的专栏 - 博客频道 - CSDN.NET

将Log4net的配置配置到的独立文件中 - 422159763 - 博客园

AutoMapper小结 - 呆河马 - 博客园

AutoMapper完成Dto与Model的转换 - kuangkro - 博客园

Quartz+TopShelf实现Windows服务作业调度 - Frozen.Zhang - 博客园

Windows服务调用Quartz.net 实现消息调度 - M.Zero - 博客园

WebApi:WebApi的Self Host模式 - MAQNH - 博客园

【MVC】ASP.NET MVC Forms验证机制 - bomo - 博客园

【WEB API项目实战干货系列】- 导航篇(十足干货分享) - DukeCheng - 博客园

ASP.NET MVC中Autofac实现的自动注入模式 - Nic Pei - 博客园

MVC5+Unity4.0注入依赖学习 - 灵猪谁仰的专栏 - 博客频道 - CSDN.NET

Unity框架的依赖注入-Dependency injection - 徐某人 - 博客园

C#软件license管理(简单软件注册机制) - CSDN博客

动手写C#注册码工具(提供源码) - 唯吴独尊 - 博客园

授权组件设计 - LicenseControlProject - Cracker - 博客园

ERP框架开发中的License许可验证机制设计与实现 (包含源代码下载) - 信息化建设 - 博客园

.NET分离exe和dll在不同的目录让你的程序更整洁 - .NET快速开发框架 - 博客园

.net RPC框架选型 - 凌晨三点半 - 博客园

[C#进阶系列]专题一:深入解析深拷贝和浅拷贝 - Learning hard - 博客园

通用的序列号生成器库 - 张善友 - 博客园

根据twitter的snowflake算法生成唯一ID - 梁照彬 - 博客园

C#动态调用WCF接口,两种方式任你选。 - Danny Chen - 博客园

WCF 学习笔记: ServiceHost - CSDN博客

使用ASP.Net WebAPI构建REST服务(六)——Self-Host - 天方 - 博客园

构建基于Chromium的应用程序 - 猫不理饼 - 博客园

8种主要排序算法的C#实现 - 胖鸟低飞 - 博客园

C#位运算 - Danny Chen - 博客园

快速排序算法(C#实现) - Eric Sun - 博客园

冒泡排序算法(C#实现) - Eric Sun - 博客园

归并排序算法(C#实现) - Eric Sun - 博客园

堆排序算法(C#实现) - Eric Sun - 博客园

插入排序算法–直接插入算法,折半排序算法,希尔排序算法(C#实现) - Eric Sun - 博客园

C# 经典排序算法大全 - 张世辉 - CSDN博客

C#排序算法小结 - hungerW - 博客园

C#排序算法的比较 - r163 - 博客园

ASP.NET MVC使用Oauth2.0实现身份验证 - 冰碟 - 博客园

Asp.Net MVC 4 Web API 中的安全认证-使用OAuth - Nic Pei - 博客园

使用Owin中间件搭建OAuth2.0认证授权服务器 - CSDN博客

使用DotNetOpenAuth搭建OAuth2.0授权框架 - 莱布尼茨 - 博客园

ASP.NET WebApi OWIN 实现 OAuth 2.0 - 田园里的蟋蟀 - 博客园

asp.net权限认证:OWIN实现OAuth 2.0 之授权码模式(Authorization Code) - 蓝建荣 - 博客园

[OAuth]基于DotNetOpenAuth实现Client Credentials Grant - dudu - 博客园

.net 单点登录实践 - 何雪峰 - 博客园

Personball’s Blog - 使用Owin中间件搭建OAuth2.0认证授权服务器

如何给你的ASP.NET页面添加HelpPage - pmars - 博客园

ASP.NET Identity登录原理 - Claims-based认证和OWIN - Jesse Liu - 推酷

如何使用FluentMigrator进行数据库迁移 - LamondLu - 博客园

领域驱动设计 - 随笔分类 - dax.net - 博客园

C# CacheHelper - Ariter - 博客园

Memcache升级版:CouchBase的安装配置与使用说明_PHP教程_开源小组_开源社区

如何用C#动态编译、执行代码 - Danny Chen - 博客园

Repository模式介绍汇总 - 跟着阿笨一起玩.NET - 博客园

8天掌握EF的Code First开发系列之5 视图、存储过程和异步API - JustYong - 博客园

1.【使用EF Code-First方式和Fluent API来探讨EF中的关系】 - 灰太狼的梦想 - 博客园

EF批量操作数据与缓存扩展框架 - 天使不哭 - 博客园

献给Fluent NHibernate的初学者 - Nic Pei - 博客园

6.数据访问技术 - 随笔分类 - Apollo.NET - 博客园

EF架构~系列目录 - 张占岭 - 博客园

Entity Framework Code First (五)Fluent API - 配置关系 - 舍长 - 博客园

wpf 实现实时毛玻璃(live blur)效果 - CSDN博客

WPF 几行代码实现窗体毛玻璃效果(Aero Glass) - CSDN博客

Wpf开源收集 - 刺客mrchenzh - 博客园

WPF开源界面库 - iGotogo - 博客园

[Prism框架实用分享]如何在主程序中合理的弹出子窗体 - @Sunth - 博客园

【WPF】右下角弹出自定义通知样式(Notification)——简单教程 - catshitone的专栏 - CSDN博客

c#,使用WPF的Adorner实现iPhone上新邮件或消息提示效果—-实现(一) - CSDN博客

WPF实现Themes切换 - CSDN博客

对TabControl的简单优化 - 猴健居士 - 博客园

[Prism框架实用分享]如何在主程序中合理的弹出子窗体 - @Sunth - 博客园

Prism - 标签 - Jason Li - 博客园

Prism - Eric_K1m的专栏 - CSDN博客

WPF中DataGrid控件内Button的Command和CommandParameter的绑定 - jumtre的专栏 - CSDN博客

Prism.Interactivity 之 PopupWindowAction 用法简记 - 不老哥 - 博客园

WPF 漂亮的自定义分页控件 - 简书

免费的精品: Productivity Power Tools 动画演示 - stg609 - 博客园

c# 模拟表单提交,post form 上传文件、大数据内容 - CSDN博客

搭建基于MongoDB的文件管理系统(一) - 作业部落 Cmd Markdown 编辑阅读器

在ASP.NET MVC中实现大文件异步上传(2) - 51CTO.COM

黄聪:如何用代码设置控制自己网站的网页在360浏览器打开时强制优先使用极速模式,而非兼容模式 - 黄聪 - 博客园

分布式架构理论篇 - CSDN博客

.NET领域驱动设计系列 - 随笔分类 - Learning hard - 博客园

矩阵的坐标变换(转) - Danny Chen - 博客园

移动端热更新方案(iOS+Android) - 七夜i - 博客园

SQL Server 2012 开发新功能 序列对象(Sequence) - zhangyoushugz - 博客园

标签: CQRS | Edison Xu’s Blog

用JS获取地址栏参数的方法(超级简单) - 墟零 - 博客园

微信开发笔记——微信网页登录授权,获取用户信息 - =金刚= - 博客园

微信公众号开发之网页授权获取用户基本信息 - 潇十一郎 - 博客园

Senparc.Weixin SDK 微信公众号 .NET 开发教程 索引 - SZW - 博客园

asp.net core 引入vue工程 - 简书

C# Socket使用以及DotNetty和Supersocket 框架 - netlock - 博客园

读取社保卡信息 - dozeoo - 博客园

.net core的配置介绍(一):IConfiguration - 没有星星的夏季 - 博客园

.Net Core DevOps -免费用Azure四步实现自动化发布(CI/CD) - 布洛克菲勒 - 博客园

Robot Framework官方教程(一)入门 - 简书

[[C#].NET/C#程序开发中如何更优美地实现失败任务重试的逻辑? | 码友网](https://codedefault.com/s/what-is-the-cleanest-way-to-write-retry-logic-in-csharp-application#:~:text=在.NET%2FC%23的程序开发中,有时候需要对一些失败的任务进行多次的重试,如果重试的次数达到我们设定的阀值,则再放弃任务,比如有以下的C%23伪代码: int retries %3D 3 %3B while (true),0) throw %3B else Thread.Sleep (1000)%3B } })

Polly .NET瞬时故障处理 - 重试_水墨长天的博客-CSDN博客_polly 重试

从零搭建一个IdentityServer——集成Asp.net core Identity - 7m鱼 - 博客园

asp.net core使用identity+jwt保护你的webapi(二)——获取jwt token - xhznl - 博客园

ASP.NET Core 6.0 添加 JWT 认证和授权 - 芦荟柚子茶 - 博客园

【Vue】Vue与ASP.NET Core WebAPI的集成 :: Garfield-加菲的博客 — 专注于IT互联网,Web技术,.Net, .Net Core,Node.js, Go语言(golang)、前端框架、项目管理、软件架构 只有原创,没有转载,只有实践,才会成文。

.Net gRPC使用Jwt验证详细教程 - 冰河洗剑

.NET 6使用Redis - Lulus - 博客园

ASP.NET Core gRPC 使用 Consul 服务注册发现 - 晓晨Master - 博客园

ASP.Net Core下Authorization的几种方式 - 简书

C#常用的加密算法:MD5、Base64、SHA1、SHA256、HmacSHA256、DES、AES、RSA - TomLucas - 博客园

VS2022 安装.NET4.5目标包 - Stay627 - 博客园

搭建面向NET Framework的CI/CD持续集成环境(一)Windows服务器安装Jenkins - ElijahZeng - 博客园

搭建面向NET Framework的CI/CD持续集成环境(二)自动代码分支编译构建 - ElijahZeng - 博客园

C#中的时间戳 - 简书

OpenCV

OpenCV - 迭代的是人,递归的是神 - 博客频道 - CSDN.NET

OpenCV入门指南 - MoreWindows Blog - 博客频道 - CSDN.NET

[使用SeetaFace和Dlib实现人脸识别 | 第七区](https://zone-7.github.io/2018/02/24/others/2018-02-24-使用SeetaFace 和Dlib实现人脸识别/)

6.Qt实时人脸识别和Sqlite数据库_哔哩哔哩_bilibili

使用OpenCV+Dlib ,多线程实现人脸检测+人脸识别(C++ ) - 知乎

Dlib学习人脸比对_dlib 人脸比对-CSDN博客

dlib库包的介绍与使用,opencv+dlib检测人脸框、opencv+dlib进行人脸68关键点检测,opencv+dlib实现人脸识别,特征聚类 - 掘金

qt+opencv+dlib 人脸识别,可以存储人脸数据_dlib qt-CSDN博客

如何计算两个人脸的相似度 - 知乎

c++ dlib + opencv + facenat 进行人脸比对 - CSDN文库

机器学习库dlib的C++编译和使用(windows和linux) - 知乎

C++

Qt Creator 配置第三方库和头文件_qt creator加载第三方库csdn-CSDN博客

Qt中开启线程的五种方法_qt 开启线程-CSDN博客

Qt事件耦合器实现(类似C#的Prism中的事件耦合器)_qt偶合-CSDN博客

c/c++编译:使用CMAKE进行跨平台开发_cmake 跨平台_随便写写。的博客-CSDN博客

C++跨平台(二):grpc和zmq的方案预研_CodeBowl的博客-CSDN博客_grpc跨平台

arm平台Openssl交叉编译_aron566的博客-CSDN博客_arm openssl交叉编译

(11条消息) Qt 添加MSVC2017编译器(2022年保姆级教程,不安装完整VS)_Copperxcx的博客-CSDN博客_msvc2017

(13条消息) C 语言实现面向对象编程_onlyshi的博客-CSDN博客_c语言面向对象的编程方法

(14条消息) Hello Qt——QtCreator代码格式化_天山老妖的博客-CSDN博客_qt 格式化代码

(14条消息) qtcreator 格式化代码_北极熊的奋斗史的博客-CSDN博客

(14条消息) Ubuntu20.10编译安装Qt4.8.7_wanyiba的博客-CSDN博客

10.搭建vs2010+Qt4.8.5+QtCreator3.4.0开发环境_xhome516的博客-CSDN博客_qt4.8.5+vs2010环境配置

在 VS2015 中使用 Qt4 - 简书

魔改 Qt Creator 插件框架(附源码)

qt plugins 插件框架_qt 插件框架-CSDN博客

深入理解QtCreator的插件设计架构 - 简书

GoLang

后端 - Go 1.18 泛型全面讲解:一篇讲清泛型的全部_个人文章 - SegmentFault 思否

【Golang】关于Go中logrus的用法 - 踏雪无痕SS - 博客园

Go Grpc Jwt身份认证 - dz45693 - 博客园

数据库:PostgreSQL加载timescaledb拓展异常FATAL: extension “timescaledb” must be preloaded_hahahafree的博客-CSDN博客

spring cloud + kubeedge_鲜卑大帝的博客-CSDN博客

[centos网卡配置详解 - vinter_he - 博客园](https://www.cnblogs.com/vinter/p/12547698.html#:~:text=centos网卡配置文件一般位于:%2Fetc%2Fsysconfig%2Fnetwork-scripts%2F 文件名一般为:ifcfg-eno或者ifcfg-eth0类似的文件,可以先用ip,addr 命令或者是ifconfig命令查看网卡信息)

最全的Python虚拟环境使用方法 - 知乎

Ubuntu:PostgreSql安装PostGis、TimeScaleDB插件_love_QJP的博客-CSDN博客

(13条消息) golang pbkdf2加密存储用户密码_藏呆羊的博客-CSDN博客

Go 实现 PBKDF2 加密算法 - beihai blog

gin集成casbin - 掘金

手把手,带你从零封装Gin框架(四):数据库初始化(GORM) - 掘金

手把手,带你从零封装Gin框架(五):静态资源处理 & 优雅重启服务器 - 掘金

Consul 入门-gRPC 服务注册与发现 - 掘金

Docker部署Go程序 - 掘金

【Vscode】调试go语言程序的最佳实践 - 腾讯云开发者社区-腾讯云

GO操作influxdb_书笑生的博客-CSDN博客_go influxdb

golang 编程规范 - 项目目录结构 - 简书

基于 Gin 模块化开发 API 框架设计_水痕01的博客-CSDN博客_gin开发api

国标

spring boot +netty 解析国标协议(GB/T 26875.3-2011)用户信息传输装置_qq_41655468的博客-CSDN博客_国标26875协议

边缘计算

(11条消息) KubeEdge环境搭建-实现原理-官方计数器示例运行成功_Counter Demo_尖耳朵的阿凡达妹妹的博客-CSDN博客_kubeedge搭建

(11条消息) 学习k8s的接口(Api)使用,并试着调用KubeEdge中(计数器demo)设备的信息_在不甘与平凡中逆水行舟的博客-CSDN博客_k8s接口

从零开始——在Ubuntu22.04系统中部署KubeEdge架构_怂怂是张的博客-CSDN博客_ubantu部署kubeedge

kubernetes - 【KubeEdge】KubeEdge部署小指南-Edge节点接入(避坑)_个人文章 - SegmentFault 思否

k8s+kubeedge+sedna部署· - 鹦鹉理 - 博客园

Linux编译安装kubeedge_beyond阿亮的博客-CSDN博客_kubeedge编译安装

在 Kubernetes 中部署并使用 KubeEdge

源码安装kubeedge_rocsdu的博客-CSDN博客_cloudcore二进制

KubeEdge安装部署 - 请务必优秀 - 博客园

Linux

gentoo 安装docker 折腾手记 - 贵贵的博客 - 开发|架构|开源|共享

【图片】Archlinux安装小记(2016.3.3)【archlinux吧】_百度贴吧

【图片】arch/manjaro安装antergos全套主题(包括DE,DM,grub2)【archlinux吧】_百度贴吧

Arch Linux 安装、配置、美化和优化 - petercao - 博客园

Centos配置国内yum源-zhuzusong-ChinaUnix博客

CentOS yum 源的配置与使用 - David_Tang - 博客园

RHEL/CentOS/Fedora各种源(EPEL、Remi、RPMForge、RPMFusion)配置_Linux教程_Linux公社-Linux系统门户网站

不得不装的CentOS的三大yum源centos

ubuntu14.04中离线安装docker - 博客频道 - CSDN.NET

CentOS-6.5离线安装docker-1.7教程 - tuzhutuzhu的专栏 - 博客频道 - CSDN.NET

Android

android实现微信自动抢红包 - oden的博客 - 博客频道 - CSDN.NET

教你一步步实现Android微信自动抢红包_Android_脚本之家

Android AccessibilityService实现微信自动抢红包 - 博客频道 - CSDN.NET

Android微信抢红包外挂 源代码 - 翊轩LeOn - 博客频道 - CSDN.NET

AndroidCompile - VideoLAN Wiki

Android唤醒、解锁屏幕代码实例_Android_脚本之家

Android程序主动点亮&解锁屏幕的实现 - 移动平台应用软件开发技术 - 博客频道 - CSDN.NET

Android辅助功能AccessibilityService与抢红包辅助_Android_脚本之家

android抢红包代码解析支持微信与QQ - Android移动开发技术文章_手机开发 - 红黑联盟

Android蓝牙搜索设备,向其发送数据并接收-刘宇 - 刘宇的博客 - CSDN博客

android 蓝牙设备监听广播 - LeslieFang - 博客园

Android总结篇系列:Android广播机制 - Windstep - 博客园

Android 蓝牙开发(一)蓝牙通信 - 奋斗的菜鸟ing - CSDN博客

Android开发之蓝牙通信(一) - AnalyzeSystem的博客 - CSDN博客

Android Bluetooth(蓝牙)实例 - Android开发教程™

android 蓝牙通信编程 - Gabriel的专栏 - CSDN博客

Android 实现蓝牙客户端与服务器端通信 - Android移动开发技术文章_手机开发 - 红黑联盟

详解Android——蓝牙技术 带你实现终端间数据传输_Android_脚本之家

android自带的示例程序 BluetoothChat 变蓝牙串口助手 - Android小子的博客笔记 - CSDN博客

Android 蓝牙开发基本流程 - q610098308的专栏 - CSDN博客

Android蓝牙串口通信模板及demo,trick - MetalSeed - CSDN博客

Android Ble 4.0 蓝牙开发交互 - Lucy__的博客 - CSDN博客

Android和BLE模块连接通信 - Spades-S - 博客园

Android BLE开发——Android手机与BLE终端通信初识 - CKTim - 博客园

Android 蓝牙4.0(BLE)开发实现对蓝牙的写入数据和读取数据 - It_BeeCoder的博客 - CSDN博客

Android游戏源码大合集(主要是AndEngine和Libgdx的) - 下载频道 - CSDN.NET

Android桌面悬浮窗效果实现,仿360手机卫士悬浮窗效果 - 下载频道 - CSDN.NET

Android滑动菜单特效实现,仿人人客户端侧滑效果demo - 下载频道 - CSDN.NET

Android教你如何一分钟实现下拉刷新功能demo - 下载频道 - CSDN.NET

Android第三方开源框架ImageLoader的完美Demo - 下载频道 - CSDN.NET

Android高手进阶之自定义View,自定义属性(带进度的圆形进度条) - 下载频道 - CSDN.NET

Android实现ListView的A-Z字母排序和过滤搜索功能 - 下载频道 - CSDN.NET

Android动画效果集合开源APP(BaseAnimation1.3) - 下载频道 - CSDN.NET

Android 通知栏Notification的全面整合学习 - 下载频道 - CSDN.NET

Android 打造史上最简单的侧滑菜单 - 下载频道 - CSDN.NET

164个完整的Java源程序代码 - 下载频道 - CSDN.NET

vlc-android源码编译过程记录 - 精神邋遢的民工 - 博客园

vlc-android 中调用用libvlcjni.so实现流媒体播放 - memegood123的专栏 - 博客频道 - CSDN.NET

vlc-android 获取MediaPlayerEncounteredError,MediaPlayerBuffering等各种事件的响应 - memegood123的专栏 - 博客频道 - CSDN.NET

【VLC-Android】vlc-android简例 - 农民伯伯 - 博客园

ffmpeg2.2在ubuntu下使用NDK编译——并在android工程下测试使用 - wainiwann - 博客园

Android版本-编译VLC - 雨の殇的天空 - 博客频道 - CSDN.NET

vlc-android源码的编译 - Find A Way 的博客 - 博客频道 - CSDN.NET

Ubuntu12.04编译vlc-android详细流程 - wainiwann - 博客园

Linux 下编译Android-VLC开源播放器详解(附源码下载) - 泡泡糖 - 博客园

LIBTOOL is undefined 问题的解决方法 - Sky_qing的专栏 - 博客频道 - CSDN.NET

基于Darwin实现的分布式流媒体直播服务器系统 - Babosa的专栏 - 博客频道 - CSDN.NET

DyncLang/DevLiveBook: 励志成为较全的直播技术导航_AnyRTC

Android wifi无线调试App新玩法ADB WIFI - 简书

WebRTC

使用WebRTC搭建前端视频聊天室——信令篇 - 说学逗唱 - SegmentFault

[部署rfc5766-turn-server--谷歌推荐的开源穿透服务器 复制链接] - 康林工作室 - 博客频道 - CSDN.NET

编译rfc5766-turn-server搭建turn服务器-码农场

Ubuntu下安装TURN Server (rfc5766-turn-server) - 不急不徐,持之以恒。 - BlogJava

随笔列表第7页 - RTC.Blacker - 博客园

C#+WebSocket+WebRTC多人语音视频系统 - 甩葱哥丶的个人空间 - 开源中国社区

一步一步搭建客服系统 (1) 3分钟实现网页版多人文本、视频聊天室 (含完整源码) - 疯吻IT - 博客园

web即时通信1–WebSocket与WebRTC的三种实现方式对比 —核心网络

在 Asp.NET MVC 中使用 SignalR 实现推送功能 - 罗朝辉(飘飘白云) - 博客频道 - CSDN.NET

java 调用wsdl的方式 - SamWu - 博客园

K8s

暴露redis-cluster到k8s集群外部.md_ll577644332的博客-CSDN博客

[(14条消息) kubernetes]-k8s部署单节点redis_爷来辣的博客-CSDN博客_k8s部署单节点redis

Docker - 实现本地镜像的导出、导入(export、import、save、load)

Microk8s 安装 与使用指南 - 腾讯云开发者社区-腾讯云

基于WSL2和Kind或Minikube:搭建Windows版Kubernetes_Kubernetes中文社区

kubeadm搭建k8s集群 - 请务必优秀 - 博客园

傻瓜式教学: Debian安装k3s(长期维护版本) | Solitudes

在 Kubernetes 上部署 Drone 持续集成环境 | Hanggi - NGNL

gitea+drone+kubernetes搭建devops平台_gitea+drone+k8s-CSDN博客

k3s containerd 配置 mirror 和 insecure - 知乎

将 minikube 的服务暴露到宿主机外 · jtr109’s Castle

嵌入式

〖嵌入式〗_叶帆的博客-CSDN博客

[耗时一周总结的嵌入式学习路线,超详细 - 知乎](https://zhuanlan.zhihu.com/p/531416610#:~:text=全文整体的学习路线: 嵌入式基础学习 -> 51单片机 -> STM32单片机 -> RTOS篇,ARM%2BLinux 每一个部分,也都从 学习内容 , 学习建议 , 学习资料 三个方面来展开,层层深入,步步指引。)

[熬夜肝了一份 C++/Linux 开发学习路线 - 帅地 - 博客园](https://www.cnblogs.com/kubidemanong/p/15151762.html#:~:text=熬夜肝了一份 C%2B%2B%2FLinux 开发学习路线 1 一、C%2B%2B 基础 (3-6个月) 2,6 六、数据结构与算法 (3-6%2B月) 7 七、项目 (2个月左右) 8 八、学习顺序)

Linux C/C++ 服务器/后端开发/后台开发学习路线_Linux服务器开发的博客-CSDN博客

VSCode配置C/C++环境 - 知乎

linux 下C语言学习路线 - lipps - 博客园

linux 下C语言学习路线_老徐拉灯的博客-CSDN博客_如何在linux中学习c语言

Linux系统开发学习路线_llhh33的博客-CSDN博客

go-logrus 日志框架封装使用 - 简书

微信小程序连接MQTT服务器全过程_白白的昕的博客-CSDN博客

这些操作删除console.log的方法,你都知道吗 - 知乎

Go语言相关书籍推荐(从入门到放弃) - 知乎

Gitea 与 Drone 集成实践:完全基于 Docker 搭建的轻量级 CI/CD 系统 - Gitea - 博客园

Gitea+Drone+Rancher CI/CD持续集成解决方案 - 掘金

有哪些值得推荐的c/c++开源框架与库 - 知乎

C语言网络编程(1)— UDP通信(这篇写得很详细,也讲了怎么借助网络调试助手)_c语言udp通信_TYINY的博客-CSDN博客

用C写一个UDP发送和接收程序_udp发送 c_11061104的博客-CSDN博客

Qt单个实例运行 - SingleApplication_yizhou2010的博客-CSDN博客

Qt应用程序的单例化(程序只运行一个实例)_qt中文件锁单例与共享内存单例_兜黎的博客-CSDN博客

C#获取硬盘序列号_lilin8905的博客-CSDN博客

消息摘要算法MD5图解及C语言实现 - 知乎

sha256 C语言实现_qq_43176116的博客-CSDN博客

HMAC-SHA256签名加密 C语言实现+例子_hmacsha256在线加密_Clnulijiayou的博客-CSDN博客

memcpy 合并数组 拷贝多个数组 memcpy采坑记_斗转星移3的博客-CSDN博客

C语言实现MD5加密,竟如此简单! - 腾讯云开发者社区-腾讯云

Golang领域模型-CQRS - Go语言中文网 - Golang中文社区

Go进阶10:logrus日志使用教程 | Go&Rust🦀

Go解决TCP粘包_go tcp粘包_向阳的野草的博客-CSDN博客

Go网络编程之UDP通信和文件传输 - 简书

kubeedge+edgemesh安装笔记_抓不到老鼠的汤姆的博客-CSDN博客

Golang最强大的访问控制框架casbin全解析 - 轩脉刃 - 博客园

ASP.NET MVC 5实现基于Quartz.net 的任务调度管理平台(一)_风神修罗使的博客-CSDN博客

Asp.Net Core2.0 基于QuartzNet任务管理系统 - ice.ko - 博客园

STM32常见通信方式(TTL、RS232、RS485、I2C,SPI,CAN)总结_有人用过bs32f103_位文杰TOP的博客-CSDN博客

Go语言实现websocket服务器 - 知乎

C++中的queue类、QT中的QQueue类 - 诺谦 - 博客园

05-部署持续部署服务-Drone(上) - 知乎

[Azure Devops] 使用 Azure Pipelines 实现 CI - 腾讯云开发者社区-腾讯云

使用Gitea+Drone来搭建自己的轻量级CI/CD自动构建平台 - 饭饭’s Blog

【小白向】基于Docker使用Gogs,Drone以及drone-runner-docker的自动化部署 - JohnBeBee - 博客园

基于etcd的服务发现与注册 - 掘金

C#实现ModBusRtu协议 - 知乎

MODBUS RTU MASTER的C语言代码 - r_jw - 博客园

FreeMobus移植在FreeRTOS上的移植(从机) - 云乐 - 博客园

grpc 双向通道 编写聊天室_grpc 双通道_tianv5的博客-CSDN博客

队列的实现(C语言)_vitobo的博客-CSDN博客

FreeRTOS(教程非常详细)_不秃也很强的博客-CSDN博客

c语言建立队列(顺序队列、循化队列和链式队列)_循环队列(数组)和链队列的建立及基本算法_落春只在无意间的博客-CSDN博客

数据结构STL——golang实现队列queue - 知乎

[eShopOnContainers 知多少1]:总体概览 - 「圣杰」 - 博客园

实现自己的.NET Core配置Provider之Yaml - BobTian - 博客园

Redis 学习笔记(六)Redis 如何实现消息队列 - 归斯君 - 博客园

【opencv实战】海康摄像rtsp流不同方案下的时延测试(Ing)_rtsp怎么测量拉流延时_昌山小屋的博客-CSDN博客

5分钟教你搭建Unity云渲染服务 - 知乎

ABP EF Core多数据库支持_abp多数据库支持_娃都会打酱油了的博客-CSDN博客

Hadoop教程 - yuan_xw的专栏 - 博客频道 - CSDN.NET

如何给变量取个简短且无歧义的名字_知识库_博客园

AutoMapper小结 - 呆河马 - 博客园

AutoMapper完成Dto与Model的转换 - kuangkro - 博客园

EF中Repository模式应用场景 - 指尖流淌 - 博客园

WPF 引用DLL纯图像资源包类库中的图片 - 耿爱学 - 博客园

WPF 如何将软件的所有图片保存在DLL内供主程序调用 - 蓝山咖啡的博客 - 博客频道 - CSDN.NET

三分钟教你学Git(十四) 之 线下传输仓库 - hongchangfirst - 博客频道 - CSDN.NET

使用epublib自动生成epub文件 - zhyoulun的专栏 - 博客频道 - CSDN.NET

如何用.net c# 读取epub格式文件 - szliszt的专栏 - 博客频道 - CSDN.NET

CMake 良心教程,教你从入门到入魂 - 知乎

(14条消息) 解决报错Cannot find module ‘webpack-cli/bin/config-yargs’_sxs7970的博客-CSDN博客_yargs 卸载

详解Docker 容器基础系统镜像打包_docker_脚本之家

一种docker基础镜像制作方法 - 老胡的笔记 - 博客频道 - CSDN.NET

Npoi操作excel - 张龙豪 - 博客园

非常详细的 Docker 学习笔记 - OPEN 开发经验库

CentOS-6.5离线安装docker-1.7教程 - tuzhutuzhu的专栏 - 博客频道 - CSDN.NET

用JIRA管理你的项目——(一)JIRA环境搭建 - Snowolf的意境空间! - ITeye技术网站

[JIRA] 最新Linux安装版本jira6.3.6安装破解以及数据导入的详细步骤-mchdba-ITPUB博客

Gradle Android最新自动化编译脚本教程(提供demo源码) - 心有灵犀鬼才心 - 博客频道 - CSDN.NET

Ubuntu20.04上安装Android Studio_ubuntu20.04安装android studio-CSDN博客

超全的auto.js基础操作,目前是autoX.js的控制方式。2023年9月23日更新!(第3/4章) - 知乎

[无障碍服务AccessibilityService详解-CSDN博客](https://blog.csdn.net/weixin_42574892/article/details/120042709#:~:text=常见无障碍服务示例 1 开关访问:允许行动不便的 Android 用户使用一个或多个开关与设备进行交互。,2 语音访问:允许行动不便的 Android 用户使用语音命令控制设备。 3 Talkback:视力受损或盲人用户常用的屏幕阅读器。)

Android 定时任务的8种实现方法 - 天涯海角路 - 博客园

通过C#将PDF快速导出为图片 - 个人文章 - SegmentFault 思否

[使用 C# 将 PDF 转换为图像](https://blog.conholdate.com/zh/total/convert-pdf-to-images-using-csharp/#:~:text=使用 C%23 将 PDF 转换为 PNG 图像 1,使用页码和输出 PNG 图片路径调用 Process (Page%2C String) 方法,将页面转换为 PNG。)

基于gitea+K3s实现DevOps/CI/CD - 掘金

InfluxDB安装以及使用 - 知乎

[c++ convert pdf to image-掘金](https://juejin.cn/s/c%2B%2B convert pdf to image)

设计一个通用的【系统操作日志】模块 - 知乎

超简单实现vr看房子、360全景图(附完整demo) - 掘金

微信小程序音视频与WebRTC互通的技术思路和实践 | 微信开放社区

实践修复 Linux 无法启动 EFI 故障 - 知乎

WPF:将调试信息输出到控制台-CSDN博客

使用 Harbor 作为 K3S 的镜像代理缓存后端 - roy2220 - 博客园

ubuntu22.04安装kubernetes1.26 (使用containerd)_ubuntu安装containerd-CSDN博客

用 Helm 在 k8s 上快速搭建 MySQL 主从集群 ,并提供对外访问 - 掘金

helm部署mysql-腾讯云开发者社区-腾讯云

ubuntu 20.4安装k8s 1.24.0、1.28.0(使用containerd)_ubuntu containerd 安装-CSDN博客

如何在 Ubuntu 22.04 上安装 Containerd 容器运行时

C# 断言 Assert - Mzhangyl - 博客园

C语言和设计模式(总结篇) 用了多年的C_c程序设计 模式-CSDN博客

Redis 集群模式与哨兵模式:详细对比与实例解析_redis cluster和哨兵区别-CSDN博客

Redis6搭建高可用的多主多从集群-阿里云开发者社区

redis集群搭建(非常详细,适合新手)_redis哨兵模式两主多从-腾讯云开发者社区-腾讯云

k8s安装redis主从版-腾讯云开发者社区-腾讯云

k8s中部署redis集群(三主三从) - 云起时。 - 博客园

【授人以渔】从根源上解决NAS影视库(Jellyfin/Emby/PLEX)刮削问题,群晖威联通华硕通用教程_NAS存储_什么值得买

etcd实现服务发现 - 烟花易冷人憔悴 - 博客园

Qt编译器迁移:从MinGW到MSVC - MrBeanC-Blog

Centos/ubuntu-搭建简单的国内代理服务器(socks5/http代理)-腾讯云开发者社区-腾讯云

2022 最新 Mac Vim 开发环境的部署与配置_macvim-CSDN博客

Gentoo网络设置 - 简书

arping工具使用_arping使用方法-CSDN博客

ping、arp、tracert三大命令详细用法,弄懂立马成大神 - 知乎

C语言函数大全及详解 - 知乎

移植Qt到ARM平台及搭建Qt交叉编译环境_qt arm-CSDN博客

嵌入式Linux开发: 从0开始编译并启动ARM Linux内核(全志)_arm linux虚机镜像怎么启动-CSDN博客

Linux系统7个运行级别(runlevel)详解 - 知乎

Linux Ubuntu 20.04 —添加开机启动(服务/脚本) - Areas - 博客园

WebApi+Grpc+Jwt身份认证 - zeran - 博客园

.NetCore使用Grpc通信,简单的微服务+JWT认证 - zeran - 博客园

网络代理神器ProxyChains快速安装配置使用 - CentOS_centos 安装proxychains-CSDN博客

amd64/UEFI/systemd/gnome/gentoo安装过程记录_gentoo安装gnome_Freeman Z的博客-CSDN博客

Gentoo安装流程分享(step by step),第一篇之基本系统的安装 - 知乎

FreeBSD Jail 使用 ipfw 进行 NAT 与端口转发 | VGOT Blog

C++后端开发路线——愿我踩过的坑,你可以绕着走-电子工程专辑

【精选】Asp.net core 身份认证框架 Microsoft Identity的使用以及如何使用Idengtity创建自带的用户模型SignInManager和UserManager的使用等等_microsoft.aspnetcore.identity_爱吃奶酪的松鼠丶的博客-CSDN博客

git 瘦身 解决 .git文件夹过大的问题_git瘦身-CSDN博客

.Net Core JWT 动态设置接口与权限,.Net Core官方的 JWT 授权验证-腾讯云开发者社区-腾讯云

asp.net core使用identity+jwt保护你的webapi(一)——identity基础配置 - xhznl - 博客园

.NET Core中JWT+OAuth2.0实现SSO,附完整源码(.NET6) - 包子wxl - 博客园

.NET 6 集成 IdentityServer4+AspNetCore Identity 读取本地数据表用户

[编程变量命名规则及单词缩写字典 - 五维思考 - 博客园](https://www.cnblogs.com/zhaoshujie/p/14983712.html#:~:text=编程单词缩写规则 1 使用变量名中每个有典型意义的单词。 如 Count of Failure 写成,ed 等。 如 Paging Request 写成 PagReq 。)

CTK完整教程(OSGI for C++ 实现 C++ Qt 模块化)-腾讯云开发者社区-腾讯云

CTK完整教程(OSGI for C++ 实现 C++ Qt 模块化) | 来唧唧歪歪(Ljjyy.com) - 多读书多实践,勤思考善领悟

编译 CTK(Win10 + Qt 5.14.0 + VS2015)_ctk编译-CSDN博客

将线程绑定在某个具体的CPU逻辑内核上运行_c# setthreadaffinitymask_皓月如我的博客-CSDN博客

Kubernetes 网络插件 Calico 完全运维指南 - 知乎

mysql5.7性能提升一百倍调优宝典_mysql5.7调优-CSDN博客

mysql5.7配置文件优化 - John-Python - 博客园

MySQL5.7配置my.ini文件_mysql5.7配置文件my.ini_It&code的博客-CSDN博客

mysql5.7配置文件详解 - 知乎

装X神器,让你的grafana看板变得炫酷起来-腾讯云开发者社区-腾讯云

Go与SOAP - Go语言中文网 - Golang中文社区

[C#]Soap服务通讯(客户端)_c# soap_eahao的博客-CSDN博客

Onvif协议:使用gSOAP创建SOAP调用实例_gsoap 生成soap_serve-CSDN博客

postgresql + pgpool 构建容灾高可用集群(数据同步流复制/主备自动切换) - 世间草木 - 博客园

K8S Calico网络插件 - RidingWind - 博客园

(三)InfluxDB入门(借助Web UI)_influxdb web界面_灵泽~的博客-CSDN博客

Docker 私有仓库搭建 - 程序员果果 - 博客园

Docker容器:本地私有仓库、harbor私有仓库部署与管理_Zhang110_的博客-CSDN博客

Docker 私有镜像仓库的搭建及认证 - 知乎

Dapper的基本使用,Insert、Update、Select、Delete-CSDN博客

QT学习笔记-QT安装postgresql驱动_postgresql 驱动_CodingPioneer的博客-CSDN博客

Qt 编译MySQL数据库驱动——MSVC版本_qt mysql 编译_英雄施工的博客-CSDN博客

[Kapacitor在Chronograf中的配置和使用 - 墨天轮](https://www.modb.pro/db/107297#:~:text=添加一个Kapacitor实例 1 在左侧导航栏中,单击配置齿轮图标。 已加载InfluxDB源列表。 2 在列表中的” Acitve Kapacitor”标题下的最右列中,找到InfluxDB源,单击”,4 单击连接按钮。 如果”连接详细信息”正确,则会显示一条成功消息,并且新部分将显示”配置警报端点”。 5 如果使用了第三方警报服务或SMTP,请更新”配置警报端点”部分中的第三方设置。 6 点击Send TestAlert按钮,会发送一条测试邮件到邮箱)

单机版Ceph环境部署,Linux平台 - 知乎

基于gin框架和jwt-go中间件实现小程序用户登陆和token验证 - zzayne - 博客园

OpenIddict 登录及详细流程解析 - 龙码精神 - 博客园

Linq动态拼接Expression表达式(可多表字段判别)_c# linq 动态分组 expression-CSDN博客

【Qt开源项目推荐】目录(持续更新)_maoboxxx的博客-CSDN博客

Github上的一些高分Qt开源项目【多图】_qt项目_雪域迷影的博客-CSDN博客

Prometheus+Grafana - 随笔分类 - 曹伟雄 - 博客园

在Linux中修改打开文件数量限制的3种方法 - 知乎

mysql中grant all privileges on赋给用户远程权限_李在奋斗的博客-CSDN博客

使用 etcd 作为服务配置中心 | Ray’s blog

.Net Core微服务系列–配置中心 - RstarYan - 博客园

使用locust进行Websocket压力测试和接口压力测试-腾讯云开发者社区-腾讯云

Loki & Promtail 详解-腾讯云开发者社区-腾讯云

(1条消息) 示例:WPF中使用Grid+Margin实现抽屉菜单效果_wpf 抽屉式菜单_He BianGu的博客-CSDN博客

最新超详细C++经典Boost库介绍_Come_code的博客-CSDN博客

Unity开源项目整理(长期整理+不定期更新)_CloudHu1989的博客-CSDN博客

ubuntu14.04中离线安装docker - 博客频道 - CSDN.NET

Docker-容器的操作 - 头痛不头痛 - 博客园

.NET Core 跨平台发布(dotnet publish) - LineZero - 博客园

常用docker命令,及一些坑 - edwardsbean的专栏 - 博客频道 - CSDN.NET

Docker常用命令 - 小爷,有点狂 - 博客频道 - CSDN.NET

Docker 常用命令 - 碉堡了 - 博客园

搭建开发环境 - React Native 中文网

百度地图内网开发项目 - javascript_net的专栏 - 博客频道 - CSDN.NET

自己动手写一个前端路由插件 - 最骚的就是你 - 博客园

Autofac 组件、服务、自动装配 《第二篇》 - 逆心 - 博客园

Asp.Net MVC及Web API框架配置会碰到的几个问题及解决方案 - 萌萌的It人 www.itmmd.com - CSDN博客

js数据验证集合、js email验证、js url验证、js长度验证、js数字验证等简单封装_表单特效_脚本之家

搭建自己的私有云服务器私有NAS系列 | 入坑大数据

CentOS6.5安装MySQL5.7详细教程 - $nail - 博客园

mysql 5.7配置项最详细的解释 - CSDN博客

Prism - 随笔分类 - 西夏 - 博客园

一步步实现 Prism + MEF(一)— 搭建框架 - 无主之城 - 博客园

WPF: WPF 中的 Triggers 和 VisualStateManager - WPInfo - 博客园

C#多线程和线程池 - 夜、微凉 - 博客园

C# List源码分析(一) - CSDN博客

c# List实现原理 - micDavid - 博客园

(粗译) Prism and WPF - 定制 Tab region adapter - 01部分 - tan_Cool - 博客园

WPF教程(十一)WPF中的命令行参数 - CSDN博客

(粗译) Prism and WPF - 定制 Tab region adapter - 02部分 - tan_Cool - 博客园

一个过滤Textbox输入的WPF Behavior - Alex Geng - 博客园

如何给 CI CD 服务器搭建上 .NET 5 构建和运行环境 - 云+社区 - 腾讯云

.net4.5部署到docker容器 - 张占岭 - 博客园

DevOps - Concourse - Anliven - 博客园

manjaro20安装后的配置、常用软件的安装、环境搭建 - Lanomw - 博客园

.net IOC框架 Unity&Autofac - 简书

解决 git 中文路径显示 unicode 代码的问题_u014028063的博客-CSDN博客_git 中文路径

(18条消息) 开发自己的gentoo LiveCD_gentoo 制作livecd_suirosu的博客-CSDN博客

开始使用gentoo linux——gentoo安装笔记(下) - devilyouwei - 博客园

(18条消息) 如何在UEFI系统上双引导 Arch Linux 和 Windows 10 ?_efi双引导切换_鸠摩智首席音效师的博客-CSDN博客

2021 Archlinux双系统安装教程(超详细) - 知乎

使用MQTTnet部署MQTT服务(转) - 吞硬币的小猪 - 博客园

一种基于RSA+AES算法实现的软件授权License设计思路(附源码) - 掘金

LSI 9217-8i 阵列卡 介绍以及卡刷 IT/IR 模式 - 简书

[Kubeedge部署指南 - KubeEdge使用文档 - 《openEuler 21.09 使用指南》 - 书栈网 · BookStack](https://www.bookstack.cn/read/openeuler-21.09-zh/KubeEdge-KubeEdge使用文档.md#4. 部署边缘端应用)

MVVM处理TreeView的SelectedItem的绑定的两种方式_treeviewitem绑定事件_lishuangquan1987的博客-CSDN博客

RK3399平台入门到精通系列讲解 - 总目录_rk3399学习_Linux 笔记的博客-CSDN博客

国标GB28181平台对接视频流 - 王纲 - 博客园

解放双手! bat 批处理自动化运行程序 - 知乎

Windows 批处理(bat)语法大全 - 赵青青 - 博客园

.net core gRPC与IdentityServer4集成认证授权 - 福禄渣渣辉 - 博客园

ESC/POS 控制指令 - ma_fighting - 博客园

小票打印ESC/POS命令集 - 简书

EPSON ESC/POS指令 - 简书

在NVMe硬盘安装ArchLinux+Windows11 | UrsusFeline-Blog

数字孪生系统开发讲解和源代码_Bigemap的博客-CSDN博客

[pgpool-ii4.1.2 高可用集群主备切换]配置部分 - 世间草木 - 博客园

JAVA-WVP+ZLMediaKit+MediaServerUI实现摄像头 GB28181推流播放录制-CSDN博客

几十款 WPF 控件 - UI 库,总有一款适合你 - 独立观察员

Virtualize Your Network on FreeBSD with VNET | Klara Inc (klarasystems.com)