view-state 구성요소로 정의. view-state는 해당 뷰를 보여준 다음, 사용자의 응답을 기다림.
<view-state id="enterBookingDetails"> <transition on="submit" to="reviewBooking" /> </view-state>
뷰 속성을 여러 방법으로 지정할 수 있다.
<view-state id="enterBookingDetails" view="bookingDetails.xhtml">
<view-state id="enterBookingDetails" view="/WEB-INF/hotels/booking/bookingDetails.xhtml>
<view-state id="enterBookingDetails" view="bookingDetails">
view-state 내부에서 유지되는 변수. Ajax 요청처럼 동일한 뷰가 여러번 보여줘야 하는 경우 유용.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.SearchCriteria" />
<on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels"/> </on-render>
뷰를 보여주기 전에 특정 액션을 실행하려면 on-render 사용.
<on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" /> </on-render>
<view-state id="enterBookingDetails" model="booking">
뷰 이벤트가 발생했을 때 지정된 모델에 대해서 다음 행동이 일어난다.
org.springframework.binding.convert.converters.TwoWayConverter을 구현하면 됨. StringToObject를 구현하는게 더 좋다.
protected abstract Object toObject(String string, Class targetClass) throws Exception; protected abstract String toString(Object object) throws Exception;
구현 예.
public class StringToMonetaryAmount extends StringToObject { public StringToMonetaryAmount() { super(MonetaryAmount.class); } @Override protected Object toObject(String string, Class targetClass) { return MonetaryAmount.valueOf(string); } @Override protected String toString(Object object) { MonetaryAmount amount = (MonetaryAmount) object; return amount.toString(); } }
org.springframework.binding.convert.converters에 이미 구현된 변환기가 위치.
org.springframework.binding.convert.service.DefaultConversionService을 상속해서 addDefaultConverters() 메소드를 재정의 하면 된다.
bind 속성으로 특정 뷰 이벤트에서 모델 바인딩과 유효성 검증을 안 하게 할 수도 있다.
<view-state id="enterBookingDetails" model="booking"> <transition on="proceed" to="reviewBooking"> <transition on="cancel" to="bookingCancelled" bind="false" /> </view-state>
binder 속성으로 바인딩 할 프로퍼티를 명시적으로 지정할 수 있음.
<view-state id="enterBookingDetails" model="booking"> <binder> <binding property="creditCard" /> <binding property="creditCardName" /> <binding property="creditCardExpiryMonth" /> <binding property="creditCardExpiryYear" /> </binder> <transition on="proceed" to="reviewBooking" /> <transition on="cancel" to="cancel" bind="false" /> </view-state>
binder로 지정하지 않으면 모든 프로퍼티를 바인딩. 변환기 지정 가능.
<view-state id="enterBookingDetails" model="booking"> <binder> <binding property="checkinDate" converter="shortDate" /> <binding property="checkoutDate" converter="shortDate" /> <binding property="creditCard" /> <binding property="creditCardName" /> <binding property="creditCardExpiryMonth" /> <binding property="creditCardExpiryYear" /> </binder> <transition on="proceed" to="reviewBooking" /> <transition on="cancel" to="cancel" bind="false" /> </view-state>
첫 번째 방법으로 유효성 검증 로직을 모델 객체 내에 정의하는 방법이다. view-state 생명주기에서 자동으로 호출 된다.
<view-state id="enterBookingDetails" model="booking"> <transition on="proceed" to="reviewBooking"> </view-state>
public class Booking { private Date checkinDate; private Date checkoutDate; ... public void validateEnterBookingDetails(ValidationContext context) { MessageContext messages = context.getMessages(); if (checkinDate.before(today())) { messages.addMessage(new MessageBuilder().error().source("checkinDate"). defaultText("Check in date must be a future date").build()); } else if (!checkinDate.before(checkoutDate)) { messages.addMessage(new MessageBuilder().error() .source("checkoutDate") .defaultText("Check out date must be later than check in date") .build()); } } }
enterBookingDetails에 대한 이벤트가 발생했을 때 자동으로 validateEnterBookingDetails이 호출 된다.
메소드 이름을 validate$<state> 로 정의하면 된다.
Validator로 불리는 별도의 객체로 정의할 수도 있다. 클래스 이름을 $<model>Validator 로 하자.
메소드 이름은 역시 validate$<state>로 하자.
@Component public class BookingValidator { public void validateEnterBookingDetails(Booking booking, ValidationContext context) { MessageContext messages = context.getMessages(); if (booking.getCheckinDate().before(today())) { messages.addMessage(new MessageBuilder().error() .source("checkinDate") .defaultText("Check in date must be a future date") .build()); } else if (!booking.getCheckinDate().before(booking.getCheckoutDate())) { messages.addMessage(new MessageBuilder().error() .source("checkoutDate") .defaultText("Check out date must be later than check in date") .build()); } } }
spring mvc의 Error 객체도 받을 수 있다.
유효성 검증 동안에 MessageContext에 접근할 수 있게 해주며, 다양한 객체에 접근 가능하게 해준다.
<view-state id="chooseAmenities" model="booking"> <transition on="proceed" to="reviewBooking"> <transition on="back" to="enterBookingDetails" validate="false" /> </view-state>
전이 대상은 (1)다른 뷰, (2)현재 뷰를 다시, (3)action을 실행, (4)Ajax 이벤트를 제어할 때
'fragments'로 불리는 일부 뷰를 보여주라는 요청일 수도 있다.
<transition on="submit" to="bookingConfirmed"> <evaluate expression="bookingAction.makeBooking(booking, messageContext)" /> </transition>
public class BookingAction { public boolean makeBooking(Booking booking, MessageContext context) { try { bookingService.make(booking); return true; } catch (RoomNotAvailableException e) { context.addMessage(builder.error().defaultText("No room is available at this hotel").build()); return false; } } }
<global-transitions> <transition on="login" to="login"> <transition on="logout" to="logout"> </global-transitions>
<transition on="event"> <!-- Handle event --> </transition>
현재 뷰 중 일부만을 다시 보여줄 수 있는 방법으로, Ajax 기반일 때 주로 사용한다.
<transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> <render fragments="searchResultsFragment" /> </transition>
','로 구분해서 다수의 fragment를 지정할 수도 있다.
MessageContext는 플로우 실행 동안에 메세지를 저장하는데 사용되는 API다.
일반 메세지나 국제화가 지원된 메세지 모두 사용 가능하다.
메세지 수준도 지정 가능하며, 지원되는 수준은 info, warning, error이 있다. 메세지를 추가할 때는 MessageBuilder를 사용하자.
MessageContext context = ... MessageBuilder builder = new MessageBuilder(); context.addMessage(builder.error().source("checkinDate").defaultText("Check in date must be a future date").build()); context.addMessage(builder.warn().source("smoking").defaultText("Smoking is bad for your health").build()); context.addMessage(builder.info().defaultText("We have processed your reservation - thank you and enjoy your stay").build());
MessageContext context = ... MessageBuilder builder = new MessageBuilder(); context.addMessage(builder.error().source("checkinDate").code("checkinDate.notFuture").build()); context.addMessage(builder.warn().source("smoking").code("notHealthy").resolvableArg("smoking").build());
스프링의 MessageSource를 사용해서 메세지 번들을 정의가 가능하다. 간단히 프로퍼티 파일로 관리하면 된다.
#messages.properties checkinDate=Check in date must be a future date notHealthy={0} is bad for your health reservationConfirmation=We have processed your reservation - thank you and enjoy your stay
뷰나 플로우에서는 resourceBundle EL 변수로 접근도 가능하다.
<h:outputText value="#{resourceBundle.reservationConfirmation}" />
시스템에서 발생한 예외에 대해 메세지 지정 가능하다. 예를 들어 타입 변환 시 예외가 발생하면 typeMismatch를 통해서 메세지 지정 가능하다.
booking.checkinDate.typeMismatch=The check in date must be in the format yyyy-mm-dd.
모달 팝업 다이얼로그를 뷰로 렌더링하고 싶다면,
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true">
특히 스프링 자바 스크립트와 함께 사용하면, 팝업을 보여주는데 클라이언트 코드가 전혀 필요 없다. SWF가 클라이언트 요청을 팝업으로 재전송(redirect)해준다.
기본적으로 브라우저의 백 버튼으로 이전 view-state로 돌아갈 수 있다. history를 사용해서 이에 대한 설정이 가능하다.
<transition on="cancel" to="bookingCancelled" history="discard">
<transition on="confirm" to="bookingConfirmed" history="invalidate">