Skip to content

5. طرق عرض Thymeleaf

لنعد إلى بنية تطبيق Spring MVC.

وصف الفصلان السابقان جوانب مختلفة من الكتلة [1]، وهي الإجراءات. وسنناقش الآن:

  • الكتلة [2] من وجهات النظر V؛
  • نموذج [3] (M) الذي تعرضه هذه العروض؛

منذ إنشاء Spring MVC، كانت تقنية JSP (Java Server Pages) هي التقنية المستخدمة لإنشاء صفحات HTML المرسلة إلى متصفحات العملاء. في السنوات الأخيرة، أصبحت تقنية [Thymeleaf] [http://www.thymeleaf.org/] متاحة أيضًا. سنقدم الآن هذه التقنية.

5.1. مشروع STS

نقوم بإنشاء مشروع جديد:

  • في [3]، حدد أن المشروع يتطلب تبعيات [Thymeleaf]. سيؤدي ذلك إلى إضافة تبعيات إطار عمل [Thymeleaf] [5] إلى تبعيات [Spring MVC] من المشروع السابق؛

الآن، دعونا نطور هذا المشروع على النحو التالي:

  

سنستلهم من المشروع السابق:

  • سيحتوي [istia.st.springmvc.controllers] على وحدات التحكم؛
  • [istia.st.springmvc.models] سيحتوي على نماذج الإجراءات والعرض؛
  • [istia.st.springmvc.main] هي الحزمة الخاصة بفئة Spring Boot القابلة للتنفيذ؛
  • [templates] سيحتوي على طرق عرض Thymeleaf؛
  • [i18n] سيحتوي على الرسائل الدولية التي تعرضها طرق العرض؛

فئة [Application] هي كما يلي:


package istia.st.springmvc.main;
 
import org.springframework.boot.SpringApplication;
 
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}

فئة [Config] هي كما يلي:


package istia.st.springmvc.main;
 
import java.util.Locale;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
 
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
 
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
 
    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}

يتيح هذا التكوين حاليًا إدارة الإعدادات المحلية.

[ViewController] كما يلي:


package istia.st.springmvc.actions;
 
import org.springframework.stereotype.Controller;
 
@Controller
public class ViewsController {
 
}
  • السطر 5: استبدلت العلامة [@Controller] العلامة [@RestController] لأن الإجراءات لن تقوم من الآن فصاعدًا بإنشاء الاستجابة للعميل. وبدلاً من ذلك، ستقوم بما يلي:
    • إنشاء نموذج M
    • إرجاع نوع [String] الذي سيكون اسم عرض [Thymeleaf] المسؤول عن عرض هذا النموذج. إن الجمع بين هذا العرض V وهذا النموذج M هو الذي سيولد دفق HTML المرسل إلى العميل؛

ملف [messages.properties] فارغ حاليًا.

5.2. [/v01]: أساسيات Thymeleaf

سنلقي نظرة على الإجراء التالي في [ViewsController]:


    // thymeleaf basics - 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • السطر 3: تعيد الإجراء نوع [String]. سيكون هذا هو اسم الإجراء؛
  • السطر 4: ستكون هذه العرض [v01]. بشكل افتراضي، يجب أن تكون موجودة في مجلد [templates] وأن يُسمى [v01.html

طريقة العرض [v01.html] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Les vues'">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="'Les vues dans Spring MVC'">Spring 4 MVC</h2>
</body>
</html>

هذا ملف HTML. وجود Thymeleaf واضح:

  • في مساحة الاسم [th] في السطر 2؛
  • في سمات [th:text] في السطرين 4 و 8؛

هذا ملف HTML صالح يمكن عرضه. نضعه في المجلد [static] [2] تحت الاسم [vue-01.html] ونصل إليه مباشرةً باستخدام متصفح:

إذا فحصنا كود مصدر الصفحة في [2]، يمكننا أن نرى أن سمات [th:text] تم إرسالها من الخادم وتجاهلها المتصفح. عندما تكون طريقة العرض نتيجة لعملية ما، يتدخل Thymeleaf ويقوم بتفسير سمات [th] قبل إرسال الاستجابة إلى العميل.

علامة HTML:


<title th:text="'Les vues'">Spring 4 MVC</title>

تتم معالجتها على النحو التالي بواسطة Thymeleaf:

  • th:text له صيغة th:text="expression" حيث expression هو تعبير يجب تقييمه. عندما يكون هذا التعبير سلسلة، كما في هذه الحالة، يجب أن يكون محاطًا بعلامات اقتباس مفردة؛
  • تحل قيمة [expression] محل نص علامة HTML، وفي هذه الحالة نص علامة [title

بعد المعالجة، تصبح العلامة أعلاه:


<title>Les vues</title>

دعونا نسمي الإجراء [/v01]:

  • في [2]، نرى عملية الاستبدال التي قام بها Thymeleaf؛

الآن دعونا نطلب عنوان URL [http://localhost:8080/v01.html]:

 

كيف يجب أن نفسر هذا؟ هل تم عرض الصفحة [templates/v01.html] مباشرةً دون المرور عبر إجراء؟ لتوضيح الأمور، نقوم بإنشاء الإجراء التالي [/v02]:


    // thymeleaf basics - 2
    @RequestMapping(value = "/v02", method = RequestMethod.GET)
    public String v02() {
        System.out.println("action v02");
        return "vue-02";
}

الطريقة [vue-02.html] هي نسخة من [v01.html]:

  

الآن دعونا نطلب عنوان URL [http://localhost:8080/vue-02.html]:

 

لم يتم العثور على عنوان URL. والآن دعونا نطلب عنوان URL [http://localhost:8080/v02.html]

  • في سجلات وحدة التحكم في [1]، نرى أنه تم استدعاء الإجراء [/v02]، مما أدى إلى عرض الصفحة [vue-02.html] في [2]؛

الآن نعلم أن عنوان URL [http://localhost:8080/v02.html] يمكن أن يشير أيضًا إلى ملف [/v02.html] في المجلد [static]. ماذا يحدث إذا كان هذا الملف موجودًا؟ دعونا نجرب ذلك. نقوم بإنشاء الملف [v02.html] التالي في المجلد [static]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2>Spring 4 MVC</h2>
</body>
</html>

ثم نطلب عنوان URL [http://localhost:8080/v02.html]:

يُظهر [1] و[2] أن الإجراء [/v02] قد تم استدعاؤه. وبالتالي يمكننا أن نستنتج أنه عندما يكون عنوان URL المطلوب بالشكل [/x.html]، فإن Spring / Thymeleaf:

  • يُنفذ الإجراء [/x] إذا كان موجودًا؛
  • يقدم الصفحة [/static/x.html] إذا كانت موجودة؛
  • يُصدر استثناء 404 Not Found في الحالات الأخرى؛

لتجنب الالتباس، من الآن فصاعدًا، لن تحمل الإجراءات والعروض نفس الأسماء.

5.3. [/v03]: تدويل العرض

يتيح تكامل Spring/Thymeleaf لـ Thymeleaf استخدام ملفات رسائل Spring. لنأخذ الإجراء الجديد التالي [/v03] كمثال:


    // internationalization of views
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

يعرض العرض التالي [vue-03.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="#{title}">Spring 4 MVC</h2>
</body>
</html>

في السطرين 4 و 8، تعبير السمة [th:text] هو #{title}، وقيمته هي رسالة المفتاح [title]. نقوم بإنشاء ملفات [messages_fr.properties] و [messages_en.properties] التالية:

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

دعونا نطلب عناوين URL [http://localhost:8080/v03.html?lang=fr] و [http://localhost:8080/v03.html?lang=en]:

لاحظ أننا قمنا بتطبيق ما تعلمناه مؤخرًا. فبدلاً من الإشارة إلى الإجراء [v03] بـ [/v03]، أشرنا إليه بـ [/v03.html].

5.4. [/v04]: إنشاء قالب M لعرض V

انظر إلى الإجراء الجديد التالي [/v04]:


    // creation of the M model of a V view
    @RequestMapping(value = "/v04", method = RequestMethod.GET)
    public String v04(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        System.out.println(String.format("Modèle=%s", model));
        return "vue-04";
}
  • السطر 4: يتم إدخال نموذج العرض في معلمات الإجراء. بشكل افتراضي، يكون هذا النموذج الأولي فارغًا. سنرى أنه من الممكن ملؤه مسبقًا؛
  • السطر 4: النموذج من النوع [Model] هو نوع من القاموس لعناصر من النوع <String, Object>. في السطر 4، نضيف إدخالاً إلى هذا القاموس بالمفتاح [person] المرتبط بقيمة من النوع [Person
  • السطر 5: نعرض النموذج على وحدة التحكم لنرى كيف يبدو؛
  • السطر 6: نعرض العرض [vue-04.html

فئة [Person] هي الفئة المستخدمة في الفصل السابق:

  

package istia.st.springmvc.models;
 
public class Personne {
 
    // identifier
    private Integer id;
    // name
    private String nom;
    // age
    private int age;
 
    // manufacturers
    public Personne() {
 
    }
 
    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }
 
    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }
 
    @Override
    public String toString() {
        return String.format("[id=%s, nom=%s,  age=%d]", id, nom, age);
    }

    // getters and setters
...
}

الطريقة [vue-04.html] هي كما يلي:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}">Nom :</span>
            <span th:text="${personne.nom}">Bill</span>
        </p>
        <p>
            <span th:text="#{personne.age}">Age :</span>
            <span th:text="${personne.age}">56</span>
        </p>
    </body>
</html>
  • يقدم السطر 10 نوعًا جديدًا من تعبيرات Thymeleaf، وهو ${var}، حيث var هو مفتاح من نموذج M الخاص بالعرض. تذكر أن الإجراء [/v04] أضاف مفتاحًا [person] إلى النموذج، مرتبطًا بنوع Person[id, name, age
  • السطر 10: يعرض اسم الشخص في النموذج؛
  • السطر 14: يعرض عمره؛

تم تعديل ملفات الرسائل لإضافة المفاتيح [person.name] و [person.age] من السطرين 9 و 13. والنتيجة هي كما يلي:

ويمكن العثور على طبيعة النموذج M في سجلات وحدة التحكم [2].

قد يتساءل المرء عن سبب عدم كتابة العرض [view-04] على النحو التالي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}"></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}" /></span>
            <span th:text="${personne.nom}"></span>
        </p>
        <p>
            <span th:text="#{personne.age}"></span>
            <span th:text="${personne.age}"></span>
        </p>
    </body>
</html>

هذه الطريقة صحيحة تمامًا وستؤدي إلى نفس النتيجة السابقة. أحد أهداف Thymeleaf هو ضمان إمكانية عرض صفحة Thymeleaf حتى لو لم تمر عبر Thymeleaf. لذا، دعونا ننشئ صفحتين ثابتتين جديدتين:

  

الصفحة [view-04b.html] هي نسخة من الصفحة [view-04.html]. وينطبق الأمر نفسه على الصفحة [view-04a.html]، لكننا أزلنا النص الثابت من الصفحة. إذا قمنا بعرض كلتا الصفحتين، فسنحصل على النتائج التالية:

في الحالة [1]، لا تظهر بنية الصفحة، بينما في الحالة [2] تظهر بوضوح. هذه هي فائدة وضع النص الثابت في عرض Thymeleaf، حتى لو تم استبداله بنص آخر أثناء وقت التشغيل.

الآن، دعونا نلقي نظرة على تفصيل تقني. في العرض [vue-04.html]، نقوم بتنسيق الكود باستخدام [Ctrl+Shift+F]. ونحصل على النتيجة التالية:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p>
        <span th:text="#{personne.nom}">Nom :</span> <span
            th:text="${personne.nom}">Bill</span>
    </p>
    <p>
        <span th:text="#{personne.age}">Age :</span> <span
            th:text="${personne.age}">56</span>
    </p>
</body>
</html>

العلامات غير متوازنة، مما يجعل قراءة الكود أكثر صعوبة. إذا قمنا بتغيير اسم [vue-04.html] إلى [vue-04.xml] وأعدنا تنسيق الكود، فستصبح العلامات متوازنة مرة أخرى. لذلك، ستكون اللاحقة [xml] أكثر عملية. من الممكن العمل باستخدام هذه اللاحقة. للقيام بذلك، نحتاج إلى تكوين Thymeleaf. لتجنب التراجع عما قمنا به، نقوم بنسخ مشروع [springmvc-vues] الذي كنا ندرسه إلى مشروع [springmvc-vues-xml]

  

نقوم بتعديل ملف [pom.xml] على النحو التالي:


    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-vues-xml</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>springmvc-vues-xml</name>
<description>Les vues dans Spring MVC</description>

تم تغيير اسم المشروع في السطرين 2 و6. بالإضافة إلى ذلك، قمنا بتغيير لاحقة العروض في مجلد [templates]:

  

يسرد المستند [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] خصائص تكوين Spring Boot التي يمكن استخدامها في ملف [application.properties]:

  

يسرد هذا المستند الخصائص التي يستخدمها Spring Boot أثناء التكوين التلقائي والتي يمكن تعديلها عن طريق تكوين [application.properties] بشكل مختلف. بالنسبة لـ Thymeleaf، فإن خصائص التكوين التلقائي هي كما يلي:


# THYMELEAF (<a href="http://github.com/spring-projects/spring-boot/tree/v1.1.9.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java">ThymeleafAutoConfiguration</a>)
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh

لذلك يمكننا ببساطة إضافة السطر


spring.thymeleaf.suffix=.xml

في [application.properties]. ومع ذلك، سنتبع نهجًا مختلفًا: التكوين عبر الكود. سنقوم بتكوين Thymeleaf في فئة [Config]:


package istia.st.springmvc.main;
 
import java.util.Locale;
 
...
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    ...
 
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCharacterEncoding("UTF-8");
     templateResolver.setCacheable(true);
        return templateResolver;
    }
 
    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }
 
}
  • تقوم الأسطر 16–24 بتكوين [TemplateResolver] لـ Thymeleaf. هذا الكائن مسؤول عن العثور على الملف المقابل بناءً على اسم العرض المقدم من قبل إجراء؛
  • تحدد السطران 18 و 19 البادئة واللاحقة المراد إضافتهما إلى اسم العرض لتحديد موقع الملف. وبالتالي، إذا كان اسم العرض هو [vue04]، فسيكون الملف المطلوب هو [classpath:/templates/vue04.xml]. [classpath:/templates] هي صيغة Spring تشير إلى مجلد [/templates] الموجود في جذر مسار فئات المشروع؛
  • السطر 21: بحيث يكون رأس HTTP في الاستجابة المرسلة إلى العميل هو:

Content-Type:text/html;charset=UTF-8
  • السطر 20: يشير إلى أن العرض يتوافق مع معيار HTML5؛
  • السطر 22: يشير إلى أنه يمكن تخزين طرق عرض Thymeleaf مؤقتًا؛
  • الأسطر 26–31: تضبط محرك تحليل العرض على زوج Spring/Thymeleaf باستخدام محرك التحليل السابق؛

دعونا نقوم بتشغيل الملف القابل للتنفيذ لهذا المشروع الجديد ونطلب عنوان URL [http://localhost:8080/v04.html?lang=en]:

 

لاحظ أنه في عنوان URL، تم استبدال الإجراء [/v04] مرة أخرى بـ [v04.html].

5.5. [/v05]: تحويل كائن إلى عرض Thymeleaf

نقوم بإنشاء الإجراء [/v05] التالي:


    // creation of the M model of a V - 2 view
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

وهو مطابق لعملية [/v04]. وفيما يلي محتوى ملف العرض [vue-05.xml]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • الأسطر 8–17: ضمن هذه الأسطر، يتم تعريف كائن Thymeleaf بواسطة السمة [th:object="${person}"] (السطر 8). هذا الكائن هو الكائن الذي يحتوي على المفتاح [person] الموجود في النموذج:
  • السطر 11: تعبير Thymeleaf [*{name}] يعادل [${object.name}] حيث [object] هو كائن Thymeleaf الحالي. لذا هنا، تعبير [*{name}] يعادل [${person.name}];
  • السطر 15: كما هو مذكور أعلاه؛

النتيجة:

 

5.6. [/v06]: الاختبارات في عرض Thymeleaf

لننظر إلى الإجراء [/v06] التالي:


    // creation of the M model of a V - 3 view
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

وهو مطابق للإجراءين السابقين. يعرض العرض التالي [vue-06.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
            <p th:if="*{age} >= 18" th:text="#{personne.majeure}">Vous êtes majeur</p>
            <p th:if="*{age} &lt; 18" th:text="#{personne.mineure}">Vous êtes mineur</p>
        </div>
    </body>
</html>
  • السطر 17: تقوم السمة [th:if] بتقييم تعبير منطقي. إذا كان هذا التعبير صحيحًا، يتم عرض العلامة؛ وإلا، لا يتم عرضها. لذا هنا، إذا كان ${person.age} >= 18، فسيتم عرض النص [#{person.majeure}]، أي مفتاح الرسالة [person.majeure] في ملفات الرسائل؛
  • السطر 18: لا يمكنك كتابة [*{age} < 18] لأن علامة < هي حرف محجوز. لذلك يجب عليك استخدام ما يعادلها في HTML [&lt;]، والمعروف أيضًا باسم كيان HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references

يتم تعديل ملفات الرسائل:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.mineure=You are under 18
personne.majeure=You are over 18

والنتيجة هي كما يلي:

5.7. [/v07]: التكرار في عرض Thymeleaf

لننظر إلى الإجراء التالي [/v07]:


    // creation of the M model of a V - 4 view
    @RequestMapping(value = "/v07", method = RequestMethod.GET)
    public String v07(Model model) {
        model.addAttribute("liste", new Personne[] { new Personne(7, "martin", 17), new Personne(8, "lucie", 32),
                new Personne(9, "paul", 7) });
        return "vue-07";
}
  • تقوم هذه العملية بإنشاء قائمة تضم ثلاثة أشخاص، وإضافتها إلى النموذج المرتبط بالمفتاح [list]، وعرض العرض [view-07

الطريقة [view-07.xml] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h3 th:text="#{liste.personnes}">Liste de personnes</h3>
        <ul>
            <li th:each="element : ${liste}" th:text="'['+ ${element.id} + ', ' +${element.nom}+ ', ' + ${element.age} + ']'">[id,nom,age]</li>
        </ul>
    </body>
</html>
  • السطر 10: تعمل السمة [th:each] على تكرار العلامة التي توجد فيها، وهي في هذه الحالة علامة <li>. ولها معلمتان هنا: [element : collection]، حيث [collection] هي مجموعة من الكائنات، وهي في هذه الحالة قائمة بالأشخاص. وسيقوم Thymeleaf بالتكرار عبر المجموعة وإنشاء عدد من علامات <li> يساوي عدد العناصر الموجودة في المجموعة. بالنسبة لكل علامة <li>، سيمثل [element] عنصر المجموعة المرتبط بالعلامة. بالنسبة لهذا العنصر، سيتم تقييم السمة [th:text]. تعبيرها هنا هو تسلسل سلاسل لإنتاج النتيجة [id, name, age
  • السطر 8: نضيف المفتاح [liste.personnes] إلى ملفات الرسائل؛

إليك النتيجة:

5.8. [/v08-/v10]: @ModelAttribute

نعود إلى موضوع تناولناه عند دراسة الإجراءات: دور التوضيح [@ModelAttribute]. سنضيف الإجراء الجديد التالي:


    // --------------- Binding and ModelAttribute ----------------------------------
 
    // if the parameter is an object, it is instantiated and possibly modified by the query parameters
    // it will automatically become part of the view model with the key [key]
    // for @ModelAttribute("xx") parameter, key will equal xx
    // for @ModelAttribute parameter, key will be equal to the parameter's lowercase class name
    // if @ModelAttribute is absent, then everything happens as if it were present without a key
    // note that this automatic presence in the model is not performed if the parameter is not a
 
    @RequestMapping(value = "/v08", method = RequestMethod.GET)
    public String v08(@ModelAttribute("someone") Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-08";
}
  • السطر 11: تضيف التعليقة التوضيحية [@ModelAttribute("someone")] تلقائيًا الكائن [Person p] إلى النموذج، مرتبطًا بالمفتاح [someone
  • السطر 12: للتحقق من النموذج؛
  • السطر 13: يعرض العرض [vue-08.xml

الطريقة [view-08.xml] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${someone}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • السطر 8: يتم تهيئة كائن Thymeleaf باستخدام كائن المفتاح [someone

والنتيجة هي كما يلي:

 

وفي وحدة التحكم، نرى السجل التالي:

Modèle={someone=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

الآن دعونا ننظر في الإجراء [/v09] التالي:


    @RequestMapping(value = "/v09", method = RequestMethod.GET)
    public String v09(Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-09";
}
  • السطر 1: يؤدي وجود المعلمة [Person p] إلى وضع الشخص [p] تلقائيًا في النموذج. ونظرًا لعدم تحديد مفتاح، فإن المفتاح المستخدم هو اسم الفئة مع تحويل الحرف الأول إلى حرف صغير. وبالتالي، فإن [Person p] تعادل [@ModelAttribute("person") Person p

الطريقة [view.09.xml] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • السطر 8: مفتاح القالب المستخدم هو [person

إليك النتيجة:

 

وسجل الدخول في وحدة تحكم الخادم:

Modèle={personne=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

الآن، لننظر إلى الإجراء الجديد التالي [/v10]:


    @ModelAttribute("uneAutrePersonne")
    private Personne getPersonne(){
        return new Personne(24,"pauline",55);
    }
 
    @RequestMapping(value = "/v10", method = RequestMethod.GET)
    public String v10(Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-10";
}
  • الأسطر 1-4: تعريف طريقة تنشئ عنصر مفتاح [anotherPerson] في النموذج لكل طلب، مرتبطًا بالكائن [new Person(24, "pauline", 55)
  • الأسطر 6-10: لا تقوم الإجراء [/v10] بأي شيء سوى تمرير النموذج الذي تتلقاه إلى العرض [vue-10.xml]. لاحظ أن المعلمة [Model model] مطلوبة فقط للعبارة الموجودة في السطر 8. وبدونها، فهي غير ضرورية؛

العرض [view-10.xml] هو كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${uneAutrePersonne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

والنتيجة هي كما يلي:

 

وسجل وحدة التحكم كما يلي:

Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55]}

5.9. [/v11]: @SessionAttributes

نعود إلى شيء رأيناه عند دراسة الإجراءات: دور التعليق التوضيحي [@SessionAttributes]. سنضيف الإجراء الجديد التالي [/v11]:


    @ModelAttribute("jean")
    private Personne getJean(){
        return new Personne(33,"jean",10);
    }
 
    @RequestMapping(value = "/v11", method = RequestMethod.GET)
    public String v11(Model model, HttpSession session) {
        System.out.println(String.format("Modèle=%s, Session[jean]=%s", model, session.getAttribute("jean")));
        return "vue-11";
}

لدينا شيء مشابه لما تناولناه للتو. يكمن الاختلاف في وجود تعليق توضيحي [@SessionAttributes] موضوع على الفئة نفسها:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • السطر 2: نحدد أن المفتاح [jean] من النموذج يجب وضعه في الجلسة؛

ولهذا السبب، في السطر 7 من الإجراء، قمنا بإدخال الجلسة. في السطر 8، نعرض قيمة الجلسة المرتبطة بالمفتاح [jean].

الطريقة [view-11.xml] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
        <hr />
        <div th:object="${session.jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

يتم عرض شخصين:

  • السطور 8–21: الشخص الذي يمتلك المفتاح [jean] في النموذج؛
  • الأسطر 23–36: الشخص الذي يمتلك المفتاح [jean] في الجلسة؛

النتائج هي كما يلي:

  • في [1]، الشخص الذي يحمل المفتاح [jean] في النموذج؛
  • في [2]، الشخص الذي يحمل المفتاح [jean] في الجلسة؛

سجل وحدة التحكم كما يلي:


Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55], jean=[id=33, nom=jean,  age=10]}, Session[jean]=null

فيما سبق، نرى أن المفتاح [jean] غير موجود في الجلسة التي تلقتها الإجراء. يمكننا أن نستنتج أن المفتاح [jean] أُضيف إلى الجلسة بعد تنفيذ الإجراء وقبل عرض الصفحة.

الآن، لننظر في الحالة التي يُشار فيها إلى مفتاح بواسطة كل من [@ModelAttribute] و [@SessionAttributes]. سننشئ الإجراءين التاليين:


    @RequestMapping(value = "/v12a", method = RequestMethod.GET)
    @ResponseBody
    public void v12a(HttpSession session) {
        session.setAttribute("paul", new Personne(51, "paul", 33));
    }
 
    // if the key of [@ModelAttribute] is also a key of [@SessionAttributes]
    // in this case, the corresponding parameter is initialized with the session value
    @RequestMapping(value = "/v12b", method = RequestMethod.GET)
    public String v12b(Model model, @ModelAttribute("paul") Personne p) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-12";
}

يُستخدم الإجراء [/v12a] فقط لتخزين العنصر ['paul', new Person(51, "paul", 33)] في الجلسة. ولا يقوم بأي شيء آخر. وحقيقة أنه مُعلم بـ [@ResponseBody] تشير إلى أنه يُنشئ الاستجابة للعميل. وبما أن نوعه هو [void]، فلا يتم إنشاء أي استجابة.

يقبل الإجراء [/v12b] [@ModelAttribute("paul") Person p] كمعلمة. إذا لم يتم القيام بأي شيء آخر، يتم إنشاء مثيل لكائن [Person] ثم تهيئته باستخدام معلمات الطلب، ولا علاقة لهذا الكائن بالكائن الذي يحمل المفتاح [paul] والموجود في الجلسة بواسطة الإجراء [/v12a]. سنضيف المفتاح [paul] إلى سمات الجلسة للفئة:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • يحتوي السطر 2 الآن على خاصيتين للجلسة؛

لنعد إلى معلمات الإجراء [/v12b]:


public String v12b(Model model, @ModelAttribute("paul") Personne p) {

الآن، لن يتم إنشاء مثيل للكائن [Person p] بل سيشير إلى الكائن الذي يحمل المفتاح [paul] في الجلسة. تبقى بقية العملية كما هي. سيظهر الكائن الذي يحمل المفتاح [paul] في قالب العرض الذي سيتم عرضه. هذا ما نريد رؤيته في السطر 11 من الإجراء [/v12b].

سيكون العرض [vue-12.xml] كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${paul}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • السطر 8: يشير إلى المفتاح [paul] من نموذج العرض؛

ينتج عن ذلك النتيجة التالية (بعد تنفيذ الإجراء [/v12a]، الذي يضع المفتاح [paul] في الجلسة):

 

سجل وحدة التحكم كما يلي:


Modèle={jean=[id=33, nom=jean,  age=10], uneAutrePersonne=[id=24, nom=pauline,  age=55], paul=[id=51, nom=paul,  age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

تمت إضافة المفتاح [paul] بنجاح إلى النموذج بالقيمة المرتبطة بالمفتاح [paul] في الجلسة.

5.10. [/v13]: إنشاء نموذج إدخال

سنناقش الآن إدخال البيانات في النموذج والتحقق من صحتها. سننشئ نموذجًا أولًا باستخدام الإجراء [/v13] التالي:


  // generates a form for entering a person
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

والذي يعرض ببساطة العرض التالي [vue-13.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v14.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${personne}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="''" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
        </form>
    </body>
</html>

إذا وضعنا هذا العرض في المجلد [static] تحت اسم [view-13.html] وطلبنا عنوان URL [http://localhost:8080/vue-13.html]، فسنحصل على الصفحة التالية:

 
  • في السطر 8 من النموذج، نجد علامة <form> مع السمة [th:action]. سيتم تقييم هذه السمة بواسطة Thymeleaf، وستحل قيمتها محل القيمة الحالية للسمة [action]، والتي هي بالتالي موجودة فقط للزينة. هنا، ستكون قيمة السمة [th:action] هي [/v14.html
  • في الأسطر 17 و23 و29، ستحل قيمة السمة [th:value] محل قيمة السمة [value]. وهنا، ستكون هذه القيمة عبارة عن سلسلة فارغة؛

عندما نطلب عنوان URL [/v13.html]، نحصل على النتيجة التالية:

 

لنلقِ نظرة على شفرة المصدر التي أنشأها Thymeleaf:


<!DOCTYPE html>
 
<html>
    <head>
        <title>Views in Spring MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/v14.html" method="post">
            <h2>Please, enter information and validate</h2>
            <div>
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td>Identifier:</td>
                            <td>
                                <input type="text" name="id" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Name:</td>
                            <td>
                                <input type="text" name="nom" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Age:</td>
                            <td>
                                <input type="text" name="age" value="" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Validate" />
        </form>
    </body>
</html>

تُظهر الأسطر 9 و18 و24 و30 تقييم Thymeleaf لسمات [th:action] و[th:value].

5.11. [/v14]: معالجة القيم المرسلة من خلال نموذج

الإجراء [/v14] هو الإجراء الذي يستقبل القيم المرسلة. وهو كما يلي:


  // processes form values
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • السطر 3: يتم تغليف القيم المرسلة في كائن [Person p]. ونعلم أن هذا الكائن يصبح تلقائيًا جزءًا من نموذج M لعرض V الذي سيتم عرضه بواسطة الإجراء، المرتبط بالمفتاح [person
  • السطر 4: العرض المعروض هو عرض [vue-14.xml

الطريقة [view-14.xml] هي كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • السطر 9: استرجاع الكائن المرتبط بمفتاح [person] من النموذج؛
  • الأسطر 12 و16 و20: نعرض خصائص هذا الكائن؛

ينتج عن ذلك النتيجة التالية:

5.12. [/v15-/v16]: التحقق من صحة النموذج

باستخدام المثال السابق، لنلقِ نظرة على التسلسل التالي:

  • في [1]، ندخل قيمًا غير صحيحة لحقول [id] و[age] من النوع [int]؛
  • في [2]، تشير استجابة الخادم إلى وجود خطأين؛

سنستخدم النموذج نفسه، ولكن في حالة حدوث أخطاء في التحقق من صحة البيانات، سنقوم بإعادة توجيه المستخدم إلى صفحة تعرض هذه الأخطاء حتى يتمكن من تصحيحها.

الإجراء [/v15] هو كما يلي:


    // ---------------------- form display
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

يستقبل النوع [SecuredPerson] التالي كمعلمة:

  

package istia.st.springmvc.models;
 
import javax.validation.constraints.NotNull;
 
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
 
public class SecuredPerson {
 
    @Range(min = 1)
    private int id;
 
    @Length(min = 4, max = 10)
    private String nom;
 
    @Range(min = 8, max = 14)
    private int age;
 
    // manufacturers
    public SecuredPerson() {
 
    }
 
    public SecuredPerson(int id, String nom, int age) {
        this.id=id;
        this.nom = nom;
        this.age = age;
    }
 
    // getters and setters
...
}

تم توضيح الحقول [id، name، age] بقيود التحقق من الصحة. العرض [view-15.xml] الذي يعرضه الإجراء [/v15] هو كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v16.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${securedPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" style="color: red">Identifiant erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>
  • الأسطر 10-47: يتم استرداد كائن نموذج الصفحة المرتبط بالمفتاح [securedPerson]. بعد طلب GET، نحصل على كائن بقيم التثبيت التالية [id=0, name=null, age=0]؛
  • السطر 17: قيمة حقل [securedPerson.id
  • السطر 20: يحدد التعبير [${#fields.hasErrors('id')}] ما إذا كانت هناك أخطاء في التحقق من صحة حقل [securedPerson.id]. إذا كان الأمر كذلك، فإن السمة [th:errors="*{id}"] تعرض رسالة الخطأ المرتبطة؛
  • يتكرر هذا السيناريو في السطر 29 للحقل [name] وفي السطر 38 للحقل [age]؛
  • السطر 45: يشير التعبير [${#fields.errors('*')}] إلى جميع الأخطاء في حقول كائن [securedPerson]. وبالتالي، فإن مجموعة هذه الأخطاء هي التي سيتم عرضها في الأسطر 44-46؛
  • السطر 16: نرى أن قيم النموذج سيتم إرسالها إلى الإجراء [/v16]. وهذا كما يلي:

    // -------------------- model validation------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // mistakes?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • السطر 3: تعمل العلامة [@Valid SecuredPerson p] على فرض التحقق من صحة القيم المرسلة؛
  • السطر 5: يتحقق مما إذا كان نموذج الإجراء غير صالح أم لا؛
  • السطر 6: إذا كان غير صالح، يتم إرجاع النموذج [vue-15.xml]. وبما أن هذا النموذج يعرض رسائل الخطأ، فسوف نرى تلك الرسائل؛
  • السطر 8: إذا تم التحقق من صحة نموذج الإجراء، فإننا نعرض العرض التالي [vue-16.xml]:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${securedPerson}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

فيما يلي بعض الأمثلة على التنفيذ:

5.13. [/v17-/v18]: التحقق من رسائل الخطأ

عند طلب الإجراء [/v15] لأول مرة، يتم الحصول على النتيجة التالية:

 

قد ترغب في الحصول على نموذج فارغ بدلاً من الأصفار في حقول [Username، Age]. لتحقيق ذلك، نقوم بتعديل نموذج الإجراء على النحو التالي:


package istia.st.springmvc.models;
 
import javax.validation.constraints.Digits;
 
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class StringSecuredPerson {
 
    @Range(min = 1)
    @Digits(fraction = 0, integer = 4)
    private String id;
 
    @Length(min = 4, max = 10)
    private String nom;
 
    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;
 
    // manufacturers
    public StringSecuredPerson() {
 
    }
 
    public StringSecuredPerson(String id, String nom, String age) {
        this.id = id;
        this.nom = nom;
        this.age = age;
    }
 
    // getters and setters
...
 
}
  • السطران 12 و 19: تم تعيين الحقلين [id] و [age] إلى النوع [String
  • السطر 11: تم تحديد أن الحقل [id] يجب أن يكون رقمًا مكونًا من أربعة أرقام كحد أقصى، بدون أرقام عشرية؛
  • السطر 18: الأمر نفسه ينطبق على حقل [age]، الذي يجب أن يكون عددًا صحيحًا مكونًا من رقمين كحد أقصى؛

يصبح الإجراء [/v17] كما يلي:


    // ---------------------- form display
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

العرض [vue-17.xml] الذي يعرضه الإجراء [/v17] هو كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v18.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${stringSecuredPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:each="err,status : ${#fields.errors('id')}" th:if="${status.index}==0" th:text="${err}" style="color: red">
                                    Identifiant erroné
                                </span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>

تم إجراء التغييرات في الأسطر التالية:

  • السطر 10: نعمل الآن مع كائن نموذج المفتاح [stringSecuredPerson
  • السطر 20: نقوم بالتكرار عبر قائمة الأخطاء لحقل [id]. في بناء الجملة [th:each="err,status : ${#fields.errors('id')}"]، تقوم المتغير [err] بالتكرار عبر القائمة. يوفر المتغير [status] معلومات حول كل تكرار. وهو كائن [index, count, size, current] حيث:
    • index: هو رقم العنصر الحالي،
    • current: هو قيمة العنصر الحالي،
    • count, size: حجم القائمة التي يتم التكرار عليها؛
  • السطر 20: نعرض فقط العنصر الأول من القائمة [th:if="${status.index}==0"] ;

الإجراء [/v18] الذي يعالج طلب POST من الإجراء [/v17] هو كما يلي:


    // -------------------- model validation------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // mistakes?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

يتم تحديث ملفات الرسائل على النحو التالي:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
notNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.id=Identifier:
personne.mineure=You are under 18
personne.majeure=You are over 18
liste.personnes=Persons' list
personne.formulaire.titre=Please, enter information and validate
personne.formulaire.valider=Validate
personne.formulaire.saisies=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=Identifier must be an integer >=1
Range.securedPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.securedPerson.nom=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer >=1
Range.stringSecuredPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.stringSecuredPerson.nom=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Should be an integer with at most four digits
Digits.stringSecuredPerson.age=Should be an integer with at most two digits

دعونا نلقي نظرة على بعض الأمثلة:

 

في [1]، نرى أن كلا المدققين لحقل [age] قد تم تنفيذهما:


    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

هل هناك ترتيب محدد لرسائل الخطأ؟ بالنسبة لحقل [age]، يبدو أن أدوات التحقق من الصحة تم تنفيذها بالترتيب [Digits، Range]. ومع ذلك، إذا قمنا بإرسال طلبات متعددة، يمكننا ملاحظة أن هذا الترتيب قد يتغير. لذلك، لا يمكننا الاعتماد على ترتيب أدوات التحقق من الصحة. في [2]، يتم عرض رسالة خطأ واحدة فقط من رسالتي الخطأ الخاصتين بحقل [id]. في [3]، يتم عرض جميع رسائل الخطأ.

5.14. [/v19-/v20]: استخدام أدوات التحقق المختلفة

لننظر إلى نموذج الإجراء الجديد التالي:

  

package istia.st.springmvc.models;
 
import java.util.Date;
 
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
 
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;
 
public class Form19 {
 
    @NotNull
    @AssertFalse
    private Boolean assertFalse;
 
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
 
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
 
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
 
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
 
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
 
    @NotNull
    @NotEmpty
    private String strNotEmpty;
 
    @NotNull
    @NotBlank
    private String strNotBlank;
 
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
 
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
 
    @NotNull
    @Email
    @NotBlank
    private String email;
 
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
 
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
 
    @URL
    @NotBlank
    private String url;
 
    // getters and setters
...
}

سيتم عرضها بواسطة الإجراء [/v19] التالي:


    // ------------------ form display
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • السطر 3: تستقبل الإجراء كائن [Form19 form] كمعلمة. إذا لم يتلق طلب GET أي معلمات، فسيتم تهيئة هذا الكائن بالقيم الافتراضية لـ Java؛
  • السطر 4: يتم عرض العرض [vue-19.xml]. وهو كما يلي:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Validations côté serveur</h3>
        <form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Contrainte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Erreur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">@NotEmpty</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@NotBlank</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotBlank}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotBlank')}" th:errors="*{strNotBlank}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertTrue</td>
                        <td class="col2">
                            <select th:field="*{assertTrue}">
                                <option value="true">True</option>
                                <option value="false">False</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Past</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Future</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Max</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Min</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Size</td>
                        <td class="col2">
                            <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Email</td>
                        <td class="col2">
                            <input type="text" th:field="*{email}" th:value="*{email}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Length</td>
                        <td class="col2">
                            <input type="text" th:field="*{str4}" th:value="*{str4}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('str4')}" th:errors="*{str4}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Range</td>
                        <td class="col2">
                            <input type="text" th:field="*{int1014}" th:value="*{int1014}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@URL</td>
                        <td class="col2">
                            <input type="text" th:field="*{url}" th:value="*{url}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

يعرض هذا الرمز العرض التالي:

 

تعرض الصفحة جدولاً مكوناً من ثلاثة أعمدة:

  • العمود 1: أداة التحقق من صحة حقل الإدخال؛
  • العمود 2: حقل الإدخال؛
  • العمود 3: رسائل الخطأ الخاصة بحقل الإدخال؛

دعونا ندرس، على سبيل المثال، كود العرض [/v19.html] لمدقق [@Pattern]:


                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
</tr>

نرى الكود الذي درسناه للتو مع نماذج [Person]:

  • السطر 2: العمود الأول: اسم أداة التحقق التي يتم اختبارها؛
  • السطر 4: ستقوم سمة Thymeleaf [th:field="*{hhmmss}] بإنشاء سمات HTML [id="hhmmss"] و [name="hhmmss"]. ستقوم سمة Thymeleaf [th:value="*{hhmmss}"] بإنشاء سمة HTML [value="قيمة [form19.hhmmss]]"؛
  • السطر 7: إذا كانت القيمة التي تم إدخالها لحقل [form19.hhmmss] غير صحيحة، فإن السطر 7 يعرض رسائل الخطأ المرتبطة بهذا الحقل؛

تتم معالجة القيم المرسلة بواسطة الإجراء [/v20] التالي:


    // ----------------- form template validation
    @RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v20(@Valid Form19 formulaire, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "vue-19";
        } else {
            // redirection to [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • السطر 3: ستملأ القيم المرسلة حقول كائن [Form19 form] إذا كانت صالحة؛
  • الأسطر 4–6: إذا كانت القيم المرسلة غير صالحة، فسيتم إعادة عرض النموذج [view-19] مع رسائل خطأ؛
  • الأسطر 6-10: إذا كانت القيم المرسلة صالحة، فسيتم إتاحة كائن [Form19] الذي تم إنشاؤه باستخدام هذه القيم للطلب التالي، وهو في هذه الحالة إعادة التوجيه. ثم يتم إتلافه؛
  • السطر 9: يتم إعادة توجيه العميل إلى الإجراء [/v19.html]. سيؤدي ذلك إلى إعادة عرض النموذج [vue-19]، الذي يحتوي على كود مثل:

<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">

ستقوم السمة [th:object="${form19}"] بعد ذلك باسترداد الكائن المرتبط بالسمة Flash [form19] وبالتالي إعادة عرض النموذج كما تم إدخاله.

يستدعي كود النموذج مزيدًا من التوضيح. انظر إلى الكود التالي:


                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
</tr>

يؤدي هذا إلى إنشاء كود HTML التالي:


<tr>
  <td class="col1">@assertFalse</td>
  <td class="col2">
    <input type="radio" value="true" id="assertFalse1" name="assertFalse" />
    <label for="assertFalse1">True</label>
    <input type="radio" value="false" id="assertFalse2" name="assertFalse" />
    <label for="assertFalse2">False</label>
  </td>
  <td class="col3">
  </td>
</tr>

في الكود


<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>

تشكل سمات Thymeleaf في السطرين 1 و 3 [th:field="*{assertFalse}"] مشكلة. لاحظنا أن هذه السمة تولد سمات HTML [id=assertFalse] و [name=assertFalse]. تنشأ الصعوبة لأن هذا يتم إنشاؤه في السطرين 1 و 3، مما يؤدي إلى وجود سمتين [name] متطابقتين وسمتين [id] متطابقتين. في حين أن هذا ممكن مع السمة [name]، إلا أنه غير ممكن مع السمة [id]. كما يظهر في كود HTML الذي تم إنشاؤه، أنشأ Thymeleaf سمتين [id] مختلفتين: [id=assertFalse1] و [id=assertFalse2]. وهذا أمر جيد. المشكلة هي أننا لا نعرف هذه المعرفات وقد نحتاج إليها. وهذا هو الحال بالنسبة لعلامة [label] في السطر 2. يجب أن تشير السمة [for] لعلامة HTML [label] إلى سمة [id]، وهي في هذه الحالة السمة التي تم إنشاؤها لعلامة [input] في السطر 1. تنص وثائق Thymeleaf على أن التعبير [${#ids.prev('assertFalse')}] يسترد آخر سمة [id] تم إنشاؤها لحقل [assertFalse].

الآن دعونا نلقي نظرة على كود القائمة المنسدلة للنموذج:


<select th:field="*{assertTrue}">
   <option value="true">True</option>
   <option value="false">False</option>
</select>

يُنشئ هذا الكود كود HTML لقائمة منسدلة:

1
2
3
4
<select id="assertTrue" name="assertTrue">
  <option value="true">True</option>
  <option value="false">False</option>
</select>

سيتم إرسال القيمة المنشورة بالاسم [name="assertTrue"].

تستخدم طريقة العرض [vue-19.xml] ورقة أنماط:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
</head>

السطر 4: يجب وضع ورقة الأنماط المستخدمة في مجلد [static] الخاص بالمشروع:

  

وفيما يلي نصه:


@CHARSET "UTF-8";
 
.col1 {
    background: lightblue;
}
 
.col2 {
    background: Cornsilk;
}
 
.col3 {
    background: #e2d31d;
}
 
.error {
    color: red;
}

الآن، لنلقِ نظرة على التواريخ:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
 
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;

يُظهر فحص حركة مرور الشبكة في Chrome DevTools (Ctrl+Shift+I) أن التواريخ يتم نشرها بتنسيق (yyyy-mm-dd):

 

ولهذا السبب تمت إضافة تعليقات توضيحية للتواريخ باستخدام أداة التحقق من الصحة:


@DateTimeFormat(pattern = "yyyy-MM-dd")

الذي يحدد التنسيق المتوقع لقيم التواريخ المنشورة.

وأخيرًا، ملف الرسائل باللغة الفرنسية [messages_fr.properties]:


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
NotNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
Future.form19.dateInFuture=La date doit être postérieure à celle d''aujourd'hui
Past.form19.dateInPast=La date doit être antérieure à celle d''aujourd'hui
Size.form19.strBetween4and6=la chaîne doit avoir entre 4 et 6 caractères
Min.form19.intMin10=La valeur doit être supérieure ou égale à 10
Max.form19.intMax100=La valeur doit être inférieure ou égale à 100
Length.form19.str4=La chaîne doit avoir quatre caractères exactement
Email.form19.email=Adresse mail invalide
URL.form19.url=URL invalide
Range.form19.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form19.hhmmss=Tapez l''heure sous la forme hh:mm:ss
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide

دعونا نلقي نظرة على بعض أمثلة التنفيذ:

 
 

في الأعلى، بين [1] و[2]، يبدو أنه لم يحدث شيء. ومع ذلك، إذا نظرنا إلى حركة مرور الشبكة (Ctrl-Shift-I)، نرى أنه كان هناك تبادلان عبر الشبكة مع الخادم:

  • في [1]، الطلب الأولي POST إلى [/v20]؛
  • في [2]، كان الرد على هذا الإجراء هو إعادة توجيه؛
  • في [3]، الطلب الثاني، هذه المرة إلى [/v19]؛

ثم يتم تنفيذ الإجراء [/v19]:


    // ------------------ form display
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • السطر 3: يتم تهيئة المعلمة [Form19 form] بسمة Flash [form19]، التي تم إنشاؤها بواسطة الإجراء السابق [/v19]، وهي كائن من النوع [Form19] يحتوي على القيم التي تم إرسالها إلى الإجراء [/v19]؛
  • السطر 4: سيتم عرض العرض [view-19.xml] مع كائن [Form19 form] في قالبه، تم تهيئته بالقيم المرسلة. وهذا هو السبب في أن المستخدم يرى النموذج تمامًا كما أرسله؛

لماذا إعادة التوجيه؟ لماذا لم نرسل ببساطة إلى الإجراء [/v19] أعلاه؟ كنا سنحصل على نفس النتيجة، مع بعض الاختلافات:

  • كان المتصفح سيدخل [http://localhost:8080/v20.html] في شريط العناوين بدلاً من [http://localhost:8080/v19.html] كما فعل هنا، لأنه يعرض آخر عنوان URL تم استدعاؤه؛
  • إذا قام المستخدم بتحديث الصفحة (F5)، فستكون النتيجة مختلفة تمامًا:
    • في حالة إعادة التوجيه، يكون عنوان URL المعروض هو [http://localhost:8080/v19.html]، الذي تم الحصول عليه عبر طلب GET. سيقوم المتصفح بإعادة تنفيذ هذا الأمر وسيتلقى بعد ذلك نموذجًا جديدًا تمامًا (يتم استخدام سمة Flash مرة واحدة فقط)،
    • في حالة عدم وجود إعادة توجيه، يكون عنوان URL المعروض هو [http://localhost:8080/v20.html]، الذي تم الحصول عليه عبر طلب POST. سيقوم المتصفح بإعادة تنفيذ هذا الأمر وبالتالي تنفيذ طلب POST آخر بنفس القيم السابقة. هنا، لا يترتب على ذلك أي عواقب، ولكنه غالبًا ما يكون غير مرغوب فيه، لذا يُفضل إعادة التوجيه عمومًا؛

5.15. [/v21-/v22]: التعامل مع أزرار الاختيار

ضع في اعتبارك مكون Spring [Lists] التالي:

  

package istia.st.springmvc.models;

import org.springframework.stereotype.Component;
 
@Component
public class Listes {
 
    private String[] deplacements = new String[] { "0", "1", "2", "3", "4" };
    private String[] libellesDeplacements = new String[] { "vélo", "marche", "train", "avion", "autre" };
    private String[] libellesBijoux = new String[] { "émeraude", "rubis", "diamant", "opaline" };
 
    // getters and setters
  ...
 
}
  • السطر 5: ستكون فئة [Lists] مكونًا من مكونات Spring؛
  • الأسطر 8–10: قوائم تُستخدم لملء أزرار الاختيار ومربعات الاختيار والقوائم المنسدلة؛

في فئة التكوين [Config]، يرد ما يلي:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • السطر 2: سيقوم Spring بفحص حزمة [models]، التي يوجد فيها مكون [Lists

نقوم بإنشاء الإجراءات الجديدة التالية:


    // ------------------ form with radio buttons
    @Autowired
    private Listes listes;
 
    @RequestMapping(value = "/v21", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v21(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-21";
    }
 
    @RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v22(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("form", formulaire);
        return "redirect:/v21.html";
}
  • السطران 2-3: يتم إدخال مكون [Lists] في وحدة التحكم؛
  • السطر 6: نتعامل مع نموذج [Form21]، الذي سنصفه لاحقًا. لاحظ أننا حددنا مفتاحه [form] في نموذج العرض. تذكر أنه بشكل افتراضي، كان سيكون [form21
  • السطر 7: نقوم بإدخال مكون [Lists] في النموذج. ستحتاج طريقة العرض إليه؛
  • السطر 8: نعرض العرض [vue-21.xml]. سيعرض هذا العرض النموذج [Form21]، وسيتم إرسال القيم المنشورة إلى الإجراء [/v22] في الأسطر 12–15؛
  • الأسطر 12-15: تقوم الإجراء [/v22] ببساطة بإعادة التوجيه إلى الإجراء [/v21]، ووضع القيم المنشورة التي تلقتها في سمة Flash بالمفتاح [form]. من المهم أن يتطابق هذا المفتاح مع المفتاح المستخدم في السطر 6؛

نموذج [Form21] هو كما يلي:

  

package istia.st.springmvc.models;
 
public class Form21 {
 
    // posted values
    private String marie = "non";
    private String deplacement = "4";
    private String[] couleurs;
    private String strCouleurs;
    private String[] bijoux;
    private String strBijoux;
    private int couleur2;
    private int[] bijoux2;
    private String strBijoux2;
 
    // getters and setters
    ...
}

الطريقة [view-21.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
 
        <h3>Formulaire - Boutons radio</h3>
        <form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" th:field="*{marie}" value="oui" />
                            <label th:for="${#ids.prev('marie')}">Oui</label>
                            <input type="radio" th:field="*{marie}" value="non" />
                            <label th:for="${#ids.prev('marie')}">Non</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{marie}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span th:each="mode, status : ${listes.deplacements}">
                                <input type="radio" th:field="*{deplacement}" th:value="${mode}" />
                                <label th:for="${#ids.prev('deplacement')}" th:text="${listes.libellesDeplacements[status.index]}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{deplacement}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • الأسطر 36–40: لاحظ استخدام مكون [Lists] في القالب لإنشاء تسميات مربعات الاختيار؛
  • يعرض العمود 3 القيمة المرسلة عبر POST، أو القيمة الأولية للنموذج أثناء طلب GET الأولي؛

يعرض هذا الرمز الصفحة التالية:

 

المطابقة لرمز HTML التالي:


<!DOCTYPE HTML>
 
<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
 
        <h3>Formulaire - Boutons radio</h3>
        <form action="/v22.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" value="oui" id="marie1" name="marie" />
                            <label for="marie1">Oui</label>
                            <input type="radio" value="non" id="marie2" name="marie" checked="checked" />
                            <label for="marie2">Non</label>
                        </td>
                        <td class="col3">
                            <span>non</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span>
                                <input type="radio" value="0" id="deplacement1" name="deplacement" />
                                <label for="deplacement1">vélo</label>
                            </span>
                            <span>
                                <input type="radio" value="1" id="deplacement2" name="deplacement" />
                                <label for="deplacement2">marche</label>
                            </span>
                            <span>
                                <input type="radio" value="2" id="deplacement3" name="deplacement" />
                                <label for="deplacement3">train</label>
                            </span>
                            <span>
                                <input type="radio" value="3" id="deplacement4" name="deplacement" />
                                <label for="deplacement4">avion</label>
                            </span>
                            <span>
                                <input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
                                <label for="deplacement5">autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span>4</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

يمكننا أن نرى أن القيم المرسلة (سمات الاسم) يتم إرسالها إلى الحقول التالية في نموذج [Form21]:


    private String marie = "non";
    private String deplacement = "4";

يُنصح القراء بإجراء الاختبارات. لاحظ أن السمة [value] الخاصة بأزرار الاختيار هي التي يتم إرسالها.

5.16. [/v23-/v24]: إدارة مربعات الاختيار

نضيف الإجراء الجديد التالي:


    // ------------------ form with checkboxes
    @RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String av20(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-23";
}
  • السطر 3: نستمر في استخدام نموذج [Form21

الطريقة [vue-23.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" th:field="*{couleurs}" value="0" />
                            <label th:for="${#ids.prev('couleurs')}">rouge</label>
                            <input type="checkbox" th:field="*{couleurs}" value="1" />
                            <label th:for="${#ids.prev('couleurs')}">vert</label>
                            <input type="checkbox" th:field="*{couleurs}" value="2" />
                            <label th:for="${#ids.prev('couleurs')}">bleu</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{strCouleurs}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span th:each="label, status : ${listes.libellesBijoux}">
                                <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
                                <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • الأسطر 37–41: لاحظ استخدام مكون [Lists] لإنشاء تسميات مربعات الاختيار؛

يعرض هذا الرمز الصفحة التالية:

 

تم إنشاؤها من كود HTML التالي:


<!DOCTYPE HTML>
 
<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/v24.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" value="0" id="couleurs1" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs1">rouge</label>
                            <input type="checkbox" value="1" id="couleurs2" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs2">vert</label>
                            <input type="checkbox" value="2" id="couleurs3" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs3">bleu</label>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span>
                                <input type="checkbox" value="0" id="bijoux1" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux1">émeraude</label>
                            </span>
                            <span>
                                <input type="checkbox" value="1" id="bijoux2" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux2">rubis</label>
                            </span>
                            <span>
                                <input type="checkbox" value="2" id="bijoux3" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux3">diamant</label>
                            </span>
                            <span>
                                <input type="checkbox" value="3" id="bijoux4" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux4">opaline</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

لاحظ أن القيم المرسلة (سمات الاسم) يتم إرسالها إلى الحقول التالية في [Form21]:


    private String[] couleurs;
    private String[] bijoux;

هذه مصفوفات لأن لكل حقل عدة خانات اختيار تحمل اسم الحقل. وبالتالي، من الممكن أن تصل عدة قيم مرسلة تحمل الاسم نفسه (سمة name في النموذج). ولذلك، يلزم وجود مصفوفة لاستردادها.

لنعد إلى كود Thymeleaf في العمود 3 من الصفحة:


  <td class="col3">
    <span th:text="*{strCouleurs}"></span>
  </td>
</tr>
<tr>
  <td class="col1">Pierres préférées</td>
  <td class="col2">
    <span th:each="label, status : ${listes.libellesBijoux}">
      <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
      <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
    </span>
  </td>
  <td class="col3">
    <span th:text="*{strBijoux}"></span>
  </td>
</tr>

الحقول المشار إليها في السطرين 2 و 14 هي كما يلي:


    private String strCouleurs;
    private String strBijoux;

يتم حسابها بواسطة الإجراء [/v24] الذي يتعامل مع طلب POST:


    // mapper Jackson / jSON
    private ObjectMapper mapper = new ObjectMapper();
 
    @RequestMapping(value = "/v24", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String av21(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
        redirectAttributes.addFlashAttribute("form", formulaire);
        formulaire.setStrCouleurs(mapper.writeValueAsString(formulaire.getCouleurs()));
        formulaire.setStrBijoux(mapper.writeValueAsString(formulaire.getBijoux()));
        return "redirect:/v23.html";
}

ضع في اعتبارك أن مكتبة Jackson/JSON مضمنة في تبعيات المشروع.

  • السطر 2: نقوم بإنشاء نوع [ObjectMapper] يتيح لنا تحويل الكائنات إلى صيغة JSON واسترجاعها منها.
  • السطر 7: نقوم بتسلسل مصفوفة الألوان إلى JSON. يتم وضع النتيجة في الحقل [strCouleurs
  • السطر 8: نقوم بتسلسل مصفوفة المجوهرات إلى JSON. يتم تخزين النتيجة في الحقل [strBijoux

فيما يلي مثال على التنفيذ:

لاحظ أن السمة [value] الخاصة بمربعات الاختيار هي التي يتم إرسالها.

5.17. [/25-/v26]: إدارة القوائم

نضيف الإجراء التالي [/v25]:


  // ------------------ form with lists
  @RequestMapping(value = "/v25", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v25(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-25";
}

الطريقة [vue-25.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
 
        <h3>Formulaire - Listes</h3>
        <form action="/someURL" th:action="@{/v26.html}" method="post"
            th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select th:field="*{couleur2}">
                                <option value="0">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{couleur2}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select th:field="*{bijoux2}" multiple="multiple" size="3">
                                <option th:each="label, status : ${listes.libellesBijoux}"
                                    th:text="${label}" th:value="${status.index}">
                                </option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux2}"></span>
                        </td>
                    </tr>
 
                </tbody>
            </table>
            <input type="submit" value="Valider" />
        </form>
    </body>
</html>
  • الأسطر 38-42: إنشاء قائمة منسدلة حيث يتم أخذ التسميات من مكون [Lists] الذي استخدمناه بالفعل؛

الصفحة المعروضة هي كما يلي:

 

تم إنشاؤها بواسطة كود HTML التالي:


<!DOCTYPE HTML>
 
<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
 
        <h3>Formulaire - Listes</h3>
        <form action="/v26.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select id="couleur2" name="couleur2">
                                <option value="0" selected="selected">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span>0</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select multiple="multiple" size="3" id="bijoux2" name="bijoux2">
                                <option value="0">émeraude</option>
                                <option value="1">rubis</option>
                                <option value="2">diamant</option>
                                <option value="3">opaline</option>
                            </select>
                            <input type="hidden" name="_bijoux2" value="1" />
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • السطر 44: لاحظ أن Thymeleaf قد أنشأ حقلًا مخفيًا. لا أفهم الغرض منه:
  • سيتم تخزين القيم المرسلة (سمات القيمة لعلامات الخيارات) في الحقول التالية (سمات الاسم) في [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • السطر 38: القائمة [jewelry2] هي قائمة متعددة الخيارات. لذلك، يمكن نشر قيم متعددة مرتبطة بالاسم [jewelry2]. لاستردادها، يجب أن يكون الحقل [jewelry2] مصفوفة. لاحظ أنها مصفوفة من الأعداد الصحيحة. وهذا ممكن لأن القيم المنشورة يمكن تحويلها إلى هذا النوع؛

يتم نشر القيم إلى الإجراء [/v26] التالي:


  @RequestMapping(value = "/v26", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
  public String v26(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
    redirectAttributes.addFlashAttribute("form", formulaire);
    formulaire.setStrBijoux2(mapper.writeValueAsString(formulaire.getBijoux2()));
    return "redirect:/v25.html";
}

لا يوجد هنا أي شيء لم نره من قبل. إليك مثال على التنفيذ:

5.18. [/v27]: تكوين الرسائل

ضع في اعتبارك الإجراء [/v27] التالي:


  // ------------------ set messages
  @RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v27(Model model) {
        model.addAttribute("param1","paramètre un");
        model.addAttribute("param2","paramètre deux");
        model.addAttribute("param3","paramètre trois");
        model.addAttribute("param4","messages.param4");        
        return "vue-27";
}

تقوم هذه العملية ببساطة بتعيين أربع قيم في النموذج وعرض العرض التالي [view-27.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{messages.titre}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2 th:text="#{messages.titre}">Spring 4 MVC</h2>
        <p th:text="#{messages.msg1(${param1})}"></p>
        <p th:text="#{messages.msg2(${param2},${param3})}"></p>
        <p th:text="#{messages.msg3(#{${param4}})}"></p>
    </body>
</html>
  • السطر 8: رسالة بدون معلمات؛
  • السطر 9: رسالة بمعلمة واحدة [$param1] مأخوذة من القالب؛
  • السطر 10: رسالة تحتوي على معلمتين [$param2, $param3] مأخوذتين من القالب؛
  • السطر 11: رسالة تحتوي على معلمة واحدة. هذه المعلمة هي نفسها مفتاح رسالة (يُشار إليه بـ #). يتم توفير المفتاح بواسطة [$param4

ملف الرسائل باللغة الفرنسية هو كما يلي:

[messages_fr.properties]


messages.titre=Messages paramétrés
messages.msg1=Un message avec un paramètre : {0}
messages.msg2=Un message avec deux paramètres : {0}, {1}
messages.msg3=Un message avec une clé de message comme paramètre : {0}
messages.param4=paramètre quatre

للإشارة إلى وجود معلمات في الرسالة، نستخدم الرموز {0}، {1}، ...

سيؤدي دمج القالب الذي تم إنشاؤه بواسطة الإجراء [/v27] مع العرض [vue-27] إلى إنتاج كود HTML التالي:


<!DOCTYPE html>
 
<html>
    <head>
        <title>Messages paramétrés</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Messages paramétrés</h2>
        <p>Un message avec un paramètre : paramètre un</p>
        <p>Un message avec deux paramètre : paramètre deux, paramètre trois</p>
        <p>Un message avec une clé de message comme paramètre : paramètre quatre</p>
    </body>
</html>

مما ينتج عنه العرض التالي:

 

ملف الرسائل باللغة الإنجليزية هو كما يلي:

[messages_fr.properties]


messages.titre=Parameterized messages
messages.msg1=Message with one parameter: {0}
messages.msg2=Message with two parameters: {0}, {1}
messages.msg3=Message with a message key as a parameter: {0}
messages.param4=parameter four

سيؤدي دمج القالب الذي تم إنشاؤه بواسطة الإجراء [/v27] مع العرض [vue-27] إلى إنتاج كود HTML التالي:


<!DOCTYPE html>
 
<html>
    <head>
        <title>Parameterized messages</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Parameterized messages</h2>
        <p>Message with one parameter: paramètre un</p>
        <p>Message with two parameters: paramètre deux, paramètre trois</p>
        <p>Message with a message key as a parameter: parameter four</p>
    </body>
</html>

مما ينتج عنه العرض التالي:

 

يمكننا أن نرى أن الرسالة الأخيرة قد تمت ترجمتها بالكامل، وهو ما لم يحدث مع الرسالتين السابقتين.

5.19. استخدام صفحة رئيسية

في تطبيق الويب، غالبًا ما تشترك طرق العرض في عدد من العناصر التي يمكن تجميعها في صفحة رئيسية. إليك مثال على ذلك:

في الأعلى، لدينا صفحتان متشابهتان حيث تم استبدال الجزء [1] بالجزء [2]. العرض هو عرض لصفحة رئيسية تحتوي على ثلاثة أجزاء ثابتة [3-5] وجزء واحد متغير [6].

5.19.1. المشروع

نحن نعمل على إنشاء مشروع [springmvc-masterpage] باتباع النهج الموضح في القسم 5.1.

  

فيما يلي ملف [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-masterpage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>springmvc-masterpage</name>
    <description>Page maître</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.springmvc.main.Main</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>

إحدى التبعيات المضمنة في هذا الملف مطلوبة للصفحة الرئيسية:

 

تتطابق حزمتا [config] و[main] مع الحزمتين اللتين تحملان نفس الاسمين في المشروع السابق.

5.19.2. الصفحة الرئيسية

  

الصفحة الرئيسية هي طريقة العرض [layout.xml] التالية:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <title>Layout</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <table style="width: 400px">
            <tr>
                <td colspan="2" bgcolor="#ccccff">
                    <div th:include="entete" />
                </td>
            </tr>
            <tr style="height: 200px">
                <td bgcolor="#ffcccc">
                    <div th:include="menu" />
                </td>
                <td>
                    <section layout:fragment="contenu">
                        <h2>Contenu</h2>
                    </section>
                </td>
            </tr>
            <tr bgcolor="#ffcc66">
                <td colspan="2">
                    <div th:include="basdepage" />
                </td>
            </tr>
        </table>
    </body>
</html>
  • السطر 2: يجب أن تحدد الصفحة الرئيسية مساحة الاسم [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]، حيث يتم استخدام أحد عناصرها في السطر 19؛
  • الأسطر 10-12: إنشاء المنطقة [1] أدناه. تسمح لك علامة Thymeleaf [th:include] بتضمين جزء محدد في ملف آخر في العرض الحالي. وهذا يسمح لك بإعادة استخدام الأجزاء عبر عروض متعددة؛
  • الأسطر 15-17: إنشاء المنطقة [2] أدناه؛
  • الأسطر 19-20: إنشاء المنطقة [3] أدناه. تنتمي السمة [layout:fragment] إلى مساحة الاسم [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. وهي تشير إلى منطقة يمكن استبدالها بأخرى أثناء وقت التشغيل؛
  • الأسطر 24–28: إنشاء المنطقة [4] أدناه؛

5.19.3. الأجزاء

الأجزاء [entete.xml] و[menu.xml] و[basdepage.xml] هي كما يلي:

[entete.xml]


<!DOCTYPE html>
<html>
    <h2>entête</h2>
</html>

[menu.xml]


<!DOCTYPE html>
<html>
    <h2>menu</h2>
</html>

[footer.xml]


<!DOCTYPE html>
<html>
    <h2>bas de page</h2>
</html>

الجزء [page1.xml] هو كما يلي:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 1</h2>
        <form action="/someURL" th:action="@{/page2.html}" method="post">
            <input type="submit" value="Page 2" />
        </form>
    </section>
</html>
  • السطر 2: تشير السمة [layout:decorator="layout"] إلى أن الصفحة الحالية [page1.xml] "مزينة"، أي أنها تنتمي إلى صفحة رئيسية. هذه هي قيمة السمة، وهي في هذه الحالة العرض [layout.xml
  • السطر 3: يحدد هذا الجزء الذي سيتم إدراج الصفحة الرئيسية [page1.xml] فيه. تشير السمة [layout:fragment="contenu"] إلى أن [page1.xml] سيتم إدراجها في الجزء المسمى [contenu]، أي المنطقة [3] من الصفحة الرئيسية؛
  • الأسطر 5-7: محتوى الجزء هو نموذج يتضمن زر POST يشير إلى الإجراء [/page2.html

الجزء [page2.xml] مشابه:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 2</h2>
        <form action="/someURL" th:action="@{/page1.html}" method="post">
            <input type="submit" value="Page 1" />
        </form>
    </section>
</html>

5.19.4. الإجراءات

 

وحدة التحكم [Layout.java] هي كما يلي:


package istia.st.springmvc.controllers;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class Layout {
    @RequestMapping(value = "/page1")
    public String page1() {
        return "page1";
    }
 
    @RequestMapping(value = "/page2", method=RequestMethod.POST)
    public String page2() {
        return "page2";
    }
}
  • الأسطر 10–12: يعرض الإجراء [/page1] ببساطة طريقة العرض [page1.xml
  • الأسطر 15-17: الأمر نفسه بالنسبة لعملية [/page2]، التي تعرض طريقة العرض [page2.xml